@ezetgalaxy/titan 25.14.4 → 25.14.6
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 +1 -1
- package/index.js +1 -1
- package/package.json +1 -1
- package/templates/app/actions/hello.js +1 -2
- package/templates/server/src/main.rs +248 -155
- package/templates/titan/bundle.js +8 -4
package/README.md
CHANGED
|
@@ -172,7 +172,7 @@ Titan now includes a built-in server-side `fetch` bridge powered by Rust.
|
|
|
172
172
|
Use it to call any external API:
|
|
173
173
|
|
|
174
174
|
```js
|
|
175
|
-
|
|
175
|
+
function hello(req) {
|
|
176
176
|
const body = JSON.stringify({
|
|
177
177
|
model: "gpt-4.1-mini",
|
|
178
178
|
messages: [{ role: "user", content: "hii" }]
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
// server/src/main.rs
|
|
2
|
+
use std::{collections::HashMap, env, fs, path::PathBuf, sync::Arc, path::Path};
|
|
2
3
|
|
|
3
4
|
use anyhow::Result;
|
|
4
5
|
use axum::{
|
|
@@ -10,23 +11,18 @@ use axum::{
|
|
|
10
11
|
Router,
|
|
11
12
|
};
|
|
12
13
|
|
|
13
|
-
use boa_engine::{
|
|
14
|
-
|
|
15
|
-
native_function::NativeFunction,
|
|
16
|
-
object::ObjectInitializer,
|
|
17
|
-
property::Attribute,
|
|
18
|
-
Context, JsValue, Source,
|
|
19
|
-
};
|
|
14
|
+
use boa_engine::{object::ObjectInitializer, Context, JsValue, Source};
|
|
15
|
+
use boa_engine::{js_string, native_function::NativeFunction, property::Attribute};
|
|
20
16
|
|
|
21
|
-
use reqwest::blocking::Client;
|
|
22
17
|
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
|
|
18
|
+
use reqwest::blocking::Client;
|
|
23
19
|
|
|
24
20
|
use serde::Deserialize;
|
|
25
21
|
use serde_json::Value;
|
|
26
|
-
|
|
27
22
|
use tokio::net::TcpListener;
|
|
28
23
|
use tokio::task;
|
|
29
24
|
|
|
25
|
+
/// Route configuration (loaded from routes.json)
|
|
30
26
|
#[derive(Debug, Deserialize)]
|
|
31
27
|
struct RouteVal {
|
|
32
28
|
r#type: String,
|
|
@@ -36,239 +32,336 @@ struct RouteVal {
|
|
|
36
32
|
#[derive(Clone)]
|
|
37
33
|
struct AppState {
|
|
38
34
|
routes: Arc<HashMap<String, RouteVal>>,
|
|
39
|
-
/// Project root — used only in dev mode
|
|
40
35
|
project_root: PathBuf,
|
|
41
36
|
}
|
|
42
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")
|
|
69
|
+
}
|
|
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()), HeaderValue::from_str(&v))
|
|
157
|
+
{
|
|
158
|
+
headers.insert(name, val);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
req = req.headers(headers);
|
|
162
|
+
}
|
|
78
163
|
|
|
79
164
|
if let Some(body) = body_opt {
|
|
80
|
-
req = req.body(body
|
|
165
|
+
req = req.body(body);
|
|
81
166
|
}
|
|
82
167
|
|
|
83
168
|
match req.send() {
|
|
84
169
|
Ok(resp) => {
|
|
85
170
|
let status = resp.status().as_u16();
|
|
86
171
|
let text = resp.text().unwrap_or_default();
|
|
87
|
-
serde_json::json!({
|
|
172
|
+
serde_json::json!({
|
|
173
|
+
"ok": true,
|
|
174
|
+
"status": status,
|
|
175
|
+
"body": text
|
|
176
|
+
})
|
|
88
177
|
}
|
|
89
|
-
Err(e) => serde_json::json!({
|
|
178
|
+
Err(e) => serde_json::json!({
|
|
179
|
+
"ok": false,
|
|
180
|
+
"error": e.to_string()
|
|
181
|
+
}),
|
|
90
182
|
}
|
|
91
183
|
});
|
|
92
184
|
|
|
93
|
-
|
|
185
|
+
// Convert serde_json::Value -> JsValue
|
|
186
|
+
Ok(JsValue::from_json(&out_json, ctx).unwrap_or(JsValue::undefined()))
|
|
94
187
|
});
|
|
95
188
|
|
|
189
|
+
// Convert native function to JS function object (requires Realm)
|
|
96
190
|
let realm = ctx.realm();
|
|
97
191
|
let t_fetch_js_fn = t_fetch_native.to_js_function(realm);
|
|
98
192
|
|
|
193
|
+
// Build `t` object with `.fetch`
|
|
99
194
|
let t_obj = ObjectInitializer::new(ctx)
|
|
100
195
|
.property(js_string!("fetch"), t_fetch_js_fn, Attribute::all())
|
|
101
196
|
.build();
|
|
102
197
|
|
|
103
198
|
ctx.global_object()
|
|
104
199
|
.set(js_string!("t"), JsValue::from(t_obj), false, ctx)
|
|
105
|
-
.
|
|
200
|
+
.expect("set global t");
|
|
106
201
|
}
|
|
107
202
|
|
|
108
|
-
|
|
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
|
-
}
|
|
130
|
-
}
|
|
203
|
+
// Root/dynamic handlers -----------------------------------------------------
|
|
131
204
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
let exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("."));
|
|
135
|
-
let exe_dir = exe.parent().unwrap_or(Path::new("/app")).to_path_buf();
|
|
136
|
-
|
|
137
|
-
// Detect production if running from Docker binary location
|
|
138
|
-
let is_prod = exe_dir.to_string_lossy().contains("/app")
|
|
139
|
-
|| exe.file_name().unwrap_or_default() == "titan-server";
|
|
140
|
-
|
|
141
|
-
if is_prod {
|
|
142
|
-
// Final production directory
|
|
143
|
-
return PathBuf::from("/app/actions");
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// DEV: exe = <root>/server/target/debug/server(.exe)
|
|
147
|
-
exe_dir
|
|
148
|
-
.parent() // target
|
|
149
|
-
.unwrap_or(Path::new("."))
|
|
150
|
-
.parent() // server
|
|
151
|
-
.unwrap_or(Path::new("."))
|
|
152
|
-
.join("actions")
|
|
205
|
+
async fn root_route(state: State<AppState>, req: Request<Body>) -> impl IntoResponse {
|
|
206
|
+
dynamic_handler_inner(state, req).await
|
|
153
207
|
}
|
|
154
208
|
|
|
209
|
+
async fn dynamic_route(state: State<AppState>, req: Request<Body>) -> impl IntoResponse {
|
|
210
|
+
dynamic_handler_inner(state, req).await
|
|
211
|
+
}
|
|
155
212
|
|
|
156
|
-
|
|
157
|
-
async fn
|
|
213
|
+
/// Main handler: looks up routes.json and executes action bundles using Boa.
|
|
214
|
+
async fn dynamic_handler_inner(
|
|
158
215
|
State(state): State<AppState>,
|
|
159
216
|
req: Request<Body>,
|
|
160
217
|
) -> impl IntoResponse {
|
|
161
218
|
let method = req.method().as_str().to_uppercase();
|
|
162
|
-
let path = req.uri().path()
|
|
219
|
+
let path = req.uri().path();
|
|
163
220
|
let key = format!("{}:{}", method, path);
|
|
164
221
|
|
|
165
222
|
let body_bytes = match to_bytes(req.into_body(), usize::MAX).await {
|
|
166
|
-
Ok(
|
|
223
|
+
Ok(b) => b,
|
|
167
224
|
Err(_) => return (StatusCode::BAD_REQUEST, "Failed to read body").into_response(),
|
|
168
225
|
};
|
|
169
226
|
let body_str = String::from_utf8_lossy(&body_bytes).to_string();
|
|
170
227
|
|
|
171
|
-
let route =
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
228
|
+
if let Some(route) = state.routes.get(&key) {
|
|
229
|
+
match route.r#type.as_str() {
|
|
230
|
+
"action" => {
|
|
231
|
+
let action_name = route.value.as_str().unwrap_or("").trim();
|
|
232
|
+
if action_name.is_empty() {
|
|
233
|
+
return (StatusCode::INTERNAL_SERVER_ERROR, "Invalid action name").into_response();
|
|
234
|
+
}
|
|
175
235
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
236
|
+
// Resolve actions directory: prefer resolve_actions_dir(), fall back to heuristic find_actions_dir
|
|
237
|
+
let resolved = resolve_actions_dir();
|
|
238
|
+
let actions_dir = if resolved.exists() && resolved.is_dir() {
|
|
239
|
+
resolved
|
|
240
|
+
} else {
|
|
241
|
+
match find_actions_dir(&state.project_root) {
|
|
242
|
+
Some(p) => p,
|
|
243
|
+
None => {
|
|
244
|
+
return (
|
|
245
|
+
StatusCode::INTERNAL_SERVER_ERROR,
|
|
246
|
+
format!("Actions directory not found (checked multiple locations)"),
|
|
247
|
+
)
|
|
248
|
+
.into_response();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
};
|
|
182
252
|
|
|
183
|
-
|
|
184
|
-
if action_name.is_empty() {
|
|
185
|
-
return (
|
|
186
|
-
StatusCode::INTERNAL_SERVER_ERROR,
|
|
187
|
-
"Invalid action name",
|
|
188
|
-
)
|
|
189
|
-
.into_response();
|
|
190
|
-
}
|
|
253
|
+
let action_path = actions_dir.join(format!("{}.jsbundle", action_name));
|
|
191
254
|
|
|
192
|
-
|
|
193
|
-
|
|
255
|
+
if !action_path.exists() {
|
|
256
|
+
return (
|
|
257
|
+
StatusCode::NOT_FOUND,
|
|
258
|
+
format!("Action bundle not found: {:?}", action_path),
|
|
259
|
+
)
|
|
260
|
+
.into_response();
|
|
261
|
+
}
|
|
194
262
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
263
|
+
let js_code = match fs::read_to_string(&action_path) {
|
|
264
|
+
Ok(v) => v,
|
|
265
|
+
Err(e) => {
|
|
266
|
+
return (
|
|
267
|
+
StatusCode::INTERNAL_SERVER_ERROR,
|
|
268
|
+
format!("Failed reading action bundle: {}", e),
|
|
269
|
+
)
|
|
270
|
+
.into_response();
|
|
271
|
+
}
|
|
272
|
+
};
|
|
202
273
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
.
|
|
211
|
-
|
|
212
|
-
|
|
274
|
+
// Build env object
|
|
275
|
+
let mut env_map = serde_json::Map::new();
|
|
276
|
+
for (k, v) in std::env::vars() {
|
|
277
|
+
env_map.insert(k, Value::String(v));
|
|
278
|
+
}
|
|
279
|
+
let env_json = Value::Object(env_map);
|
|
280
|
+
|
|
281
|
+
// Injected script: sets process.env and __titan_req and invokes action function.
|
|
282
|
+
let injected = format!(
|
|
283
|
+
r#"
|
|
284
|
+
globalThis.process = {{ env: {} }};
|
|
285
|
+
const __titan_req = {};
|
|
286
|
+
{};
|
|
287
|
+
{}(__titan_req);
|
|
288
|
+
"#,
|
|
289
|
+
env_json.to_string(),
|
|
290
|
+
body_str,
|
|
291
|
+
js_code,
|
|
292
|
+
action_name
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
let mut ctx = Context::default();
|
|
296
|
+
inject_t_fetch(&mut ctx);
|
|
297
|
+
|
|
298
|
+
let result = match ctx.eval(Source::from_bytes(&injected)) {
|
|
299
|
+
Ok(v) => v,
|
|
300
|
+
Err(e) => return Json(json_error(e.to_string())).into_response(),
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
let result_json: Value = match result.to_json(&mut ctx) {
|
|
304
|
+
Ok(v) => v,
|
|
305
|
+
Err(e) => json_error(e.to_string()),
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
return Json(result_json).into_response();
|
|
309
|
+
}
|
|
213
310
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
217
319
|
}
|
|
218
|
-
let env_json = Value::Object(env_map);
|
|
219
|
-
|
|
220
|
-
let injected = format!(
|
|
221
|
-
r#"
|
|
222
|
-
globalThis.process = {{ env: {} }};
|
|
223
|
-
const __titan_req = {};
|
|
224
|
-
{};
|
|
225
|
-
{}(__titan_req);
|
|
226
|
-
"#,
|
|
227
|
-
env_json.to_string(),
|
|
228
|
-
body_str,
|
|
229
|
-
js_code,
|
|
230
|
-
action_name
|
|
231
|
-
);
|
|
232
|
-
|
|
233
|
-
let mut ctx = Context::default();
|
|
234
|
-
inject_t_fetch(&mut ctx);
|
|
235
|
-
|
|
236
|
-
let result = match ctx.eval(Source::from_bytes(&injected)) {
|
|
237
|
-
Ok(v) => v,
|
|
238
|
-
Err(e) => return Json(serde_json::json!({ "error": e.to_string() })).into_response(),
|
|
239
|
-
};
|
|
240
320
|
|
|
241
|
-
|
|
242
|
-
|
|
321
|
+
(StatusCode::NOT_FOUND, "Not Found").into_response()
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
fn json_error(msg: String) -> Value {
|
|
325
|
+
serde_json::json!({ "error": msg })
|
|
243
326
|
}
|
|
244
327
|
|
|
328
|
+
// Entrypoint ---------------------------------------------------------------
|
|
329
|
+
|
|
245
330
|
#[tokio::main]
|
|
246
331
|
async fn main() -> Result<()> {
|
|
247
332
|
dotenvy::dotenv().ok();
|
|
248
333
|
|
|
249
|
-
|
|
334
|
+
// Load routes.json (expected at runtime root)
|
|
335
|
+
let raw = fs::read_to_string("./routes.json").unwrap_or_else(|_| "{}".to_string());
|
|
250
336
|
let json: Value = serde_json::from_str(&raw).unwrap_or_default();
|
|
251
337
|
|
|
252
338
|
let port = json["__config"]["port"].as_u64().unwrap_or(3000);
|
|
253
|
-
let
|
|
254
|
-
|
|
339
|
+
let routes_json = json["routes"].clone();
|
|
340
|
+
let map: HashMap<String, RouteVal> = serde_json::from_value(routes_json).unwrap_or_default();
|
|
255
341
|
|
|
256
|
-
|
|
342
|
+
// Project root — heuristics: try current_dir()
|
|
343
|
+
let project_root = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
|
257
344
|
|
|
258
|
-
let state = AppState {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
|
|
345
|
+
let state = AppState {
|
|
346
|
+
routes: Arc::new(map),
|
|
347
|
+
project_root,
|
|
348
|
+
};
|
|
264
349
|
|
|
265
350
|
let app = Router::new()
|
|
266
|
-
.route("/", any(
|
|
267
|
-
.fallback(any(
|
|
351
|
+
.route("/", any(root_route))
|
|
352
|
+
.fallback(any(dynamic_route))
|
|
268
353
|
.with_state(state);
|
|
269
354
|
|
|
270
355
|
let listener = TcpListener::bind(format!("0.0.0.0:{}", port)).await?;
|
|
271
|
-
|
|
356
|
+
|
|
357
|
+
// Banner (yellow-orange) and server info
|
|
358
|
+
println!("\n\x1b[38;5;208m████████╗██╗████████╗ █████╗ ███╗ ██╗");
|
|
359
|
+
println!("╚══██╔══╝██║╚══██╔══╝██╔══██╗████╗ ██║");
|
|
360
|
+
println!(" ██║ ██║ ██║ ███████║██╔██╗ ██║");
|
|
361
|
+
println!(" ██║ ██║ ██║ ██╔══██║██║╚██╗██║");
|
|
362
|
+
println!(" ██║ ██║ ██║ ██║ ██║██║ ╚████║");
|
|
363
|
+
println!(" ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝\x1b[0m\n");
|
|
364
|
+
println!("\x1b[38;5;39mTitan server running at:\x1b[0m http://localhost:{}", port);
|
|
272
365
|
|
|
273
366
|
axum::serve(listener, app).await?;
|
|
274
367
|
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.");
|