@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 +344 -339
- package/index.js +1 -1
- package/package.json +1 -1
- package/templates/app/actions/hello.js +1 -2
- package/templates/server/src/main.rs +280 -164
- package/templates/titan/bundle.js +8 -4
package/README.md
CHANGED
|
@@ -1,339 +1,344 @@
|
|
|
1
|
-
|
|
2
|
-
```
|
|
3
|
-
████████╗██╗████████╗ █████╗ ███╗ ██╗
|
|
4
|
-
╚══██╔══╝██║╚══██╔══╝██╔══██╗████╗ ██║
|
|
5
|
-
██║ ██║ ██║ ███████║██╔██╗ ██║
|
|
6
|
-
██║ ██║ ██║ ██╔══██║██║╚██╗██║
|
|
7
|
-
██║ ██║ ██║ ██║ ██║██║ ╚████║
|
|
8
|
-
╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
Titan
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
|
30
|
-
|
|
|
31
|
-
|
|
|
32
|
-
|
|
|
33
|
-
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
#
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
───────────────────────────────────────────────────────────────
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
│
|
|
121
|
-
├──
|
|
122
|
-
│
|
|
123
|
-
│
|
|
124
|
-
|
|
125
|
-
│ ├──
|
|
126
|
-
│ ├──
|
|
127
|
-
│ ├──
|
|
128
|
-
│
|
|
129
|
-
│
|
|
130
|
-
├──
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
t
|
|
148
|
-
|
|
149
|
-
t.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
*
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
git
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
*
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,34 +1,28 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
|
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
|
-
.
|
|
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")
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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!({
|
|
173
|
+
serde_json::json!({
|
|
174
|
+
"ok": true,
|
|
175
|
+
"status": status,
|
|
176
|
+
"body": text
|
|
177
|
+
})
|
|
88
178
|
}
|
|
89
|
-
Err(e) => serde_json::json!({
|
|
179
|
+
Err(e) => serde_json::json!({
|
|
180
|
+
"ok": false,
|
|
181
|
+
"error": e.to_string()
|
|
182
|
+
}),
|
|
90
183
|
}
|
|
91
184
|
});
|
|
92
185
|
|
|
93
|
-
|
|
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
|
-
.
|
|
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
|
|
134
|
-
|
|
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
|
|
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()
|
|
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(
|
|
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 =
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
234
|
-
|
|
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
|
-
|
|
237
|
-
|
|
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
|
-
|
|
242
|
-
|
|
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
|
-
|
|
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
|
|
254
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
266
|
-
.fallback(any(
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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: "
|
|
33
|
+
format: "iife",
|
|
33
34
|
platform: "neutral",
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
target: "es2020",
|
|
36
|
+
banner: {
|
|
37
|
+
js: ""
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
console.log("[Titan] Bundling finished.");
|