@ezetgalaxy/titan 25.13.6 → 25.14.2
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/index.js +34 -20
- package/package.json +1 -1
- package/templates/server/src/main.rs +144 -218
package/index.js
CHANGED
|
@@ -189,50 +189,64 @@ function buildProd() {
|
|
|
189
189
|
const appJs = path.join(root, "app", "app.js");
|
|
190
190
|
const bundler = path.join(root, "titan", "bundle.js");
|
|
191
191
|
const serverDir = path.join(root, "server");
|
|
192
|
-
const builtActionsDir = path.join(
|
|
193
|
-
const actionsOut =
|
|
192
|
+
const builtActionsDir = path.join(serverDir, "actions"); // REAL output
|
|
193
|
+
const actionsOut = builtActionsDir; // final output
|
|
194
194
|
|
|
195
|
-
//
|
|
195
|
+
// --------------------------------------------
|
|
196
|
+
// 0) Basic validation
|
|
197
|
+
// --------------------------------------------
|
|
196
198
|
if (!fs.existsSync(appJs)) {
|
|
197
199
|
console.log(red("ERROR: app/app.js not found."));
|
|
198
200
|
process.exit(1);
|
|
199
201
|
}
|
|
200
202
|
|
|
201
|
-
// Ensure bundler exists
|
|
202
203
|
if (!fs.existsSync(bundler)) {
|
|
203
204
|
console.log(red("ERROR: titan/bundle.js not found."));
|
|
204
205
|
process.exit(1);
|
|
205
206
|
}
|
|
206
207
|
|
|
207
|
-
//
|
|
208
|
+
// --------------------------------------------
|
|
209
|
+
// 1) Generate routes + action_map via DSL
|
|
210
|
+
// --------------------------------------------
|
|
208
211
|
console.log(cyan("→ Generating Titan metadata..."));
|
|
209
212
|
execSync("node app/app.js --build", { stdio: "inherit" });
|
|
210
213
|
|
|
214
|
+
// --------------------------------------------
|
|
211
215
|
// 2) Bundle JS actions
|
|
216
|
+
// --------------------------------------------
|
|
212
217
|
console.log(cyan("→ Bundling Titan actions..."));
|
|
213
218
|
execSync("node titan/bundle.js", { stdio: "inherit" });
|
|
214
219
|
|
|
220
|
+
// --------------------------------------------
|
|
215
221
|
// 3) Ensure server/actions exists
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
222
|
+
// --------------------------------------------
|
|
223
|
+
fs.mkdirSync(actionsOut, { recursive: true });
|
|
224
|
+
|
|
225
|
+
// --------------------------------------------
|
|
226
|
+
// 4) List & verify bundles
|
|
227
|
+
// --------------------------------------------
|
|
228
|
+
if (!fs.existsSync(builtActionsDir)) {
|
|
229
|
+
console.log(red("ERROR: Titan bundler did NOT generate server/actions directory."));
|
|
230
|
+
console.log(red("This means titan/bundle.js output path is incorrect."));
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const bundles = fs.readdirSync(builtActionsDir)
|
|
235
|
+
.filter(f => f.endsWith(".jsbundle"));
|
|
236
|
+
|
|
237
|
+
if (bundles.length === 0) {
|
|
230
238
|
console.log(yellow("Warning: No actions bundled."));
|
|
239
|
+
} else {
|
|
240
|
+
for (const file of bundles) {
|
|
241
|
+
console.log(cyan(`→ Found bundle: ${file}`));
|
|
242
|
+
}
|
|
231
243
|
}
|
|
232
244
|
|
|
233
245
|
console.log(green("✔ Actions copied to server/actions"));
|
|
234
246
|
|
|
235
|
-
//
|
|
247
|
+
// --------------------------------------------
|
|
248
|
+
// 5) Build Rust server
|
|
249
|
+
// --------------------------------------------
|
|
236
250
|
console.log(cyan("→ Building Rust release binary..."));
|
|
237
251
|
execSync("cargo build --release", {
|
|
238
252
|
cwd: serverDir,
|
package/package.json
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// server/src/main.rs
|
|
2
1
|
use std::{collections::HashMap, fs, path::PathBuf, sync::Arc};
|
|
3
2
|
|
|
4
3
|
use anyhow::Result;
|
|
@@ -11,18 +10,23 @@ use axum::{
|
|
|
11
10
|
Router,
|
|
12
11
|
};
|
|
13
12
|
|
|
14
|
-
use boa_engine::{
|
|
15
|
-
|
|
13
|
+
use boa_engine::{
|
|
14
|
+
js_string,
|
|
15
|
+
native_function::NativeFunction,
|
|
16
|
+
object::ObjectInitializer,
|
|
17
|
+
property::Attribute,
|
|
18
|
+
Context, JsValue, Source,
|
|
19
|
+
};
|
|
16
20
|
|
|
17
|
-
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
|
|
18
21
|
use reqwest::blocking::Client;
|
|
22
|
+
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
|
|
19
23
|
|
|
20
24
|
use serde::Deserialize;
|
|
21
25
|
use serde_json::Value;
|
|
26
|
+
|
|
22
27
|
use tokio::net::TcpListener;
|
|
23
28
|
use tokio::task;
|
|
24
29
|
|
|
25
|
-
/// Route configuration (loaded from routes.json)
|
|
26
30
|
#[derive(Debug, Deserialize)]
|
|
27
31
|
struct RouteVal {
|
|
28
32
|
r#type: String,
|
|
@@ -32,307 +36,229 @@ struct RouteVal {
|
|
|
32
36
|
#[derive(Clone)]
|
|
33
37
|
struct AppState {
|
|
34
38
|
routes: Arc<HashMap<String, RouteVal>>,
|
|
39
|
+
/// Project root — used only in dev mode
|
|
35
40
|
project_root: PathBuf,
|
|
36
41
|
}
|
|
37
42
|
|
|
38
|
-
/// Try to find the directory that contains compiled action bundles.
|
|
39
|
-
///
|
|
40
|
-
/// Checks multiple likely paths to support both dev and production container layouts:
|
|
41
|
-
/// - <project_root>/server/actions
|
|
42
|
-
/// - <project_root>/actions
|
|
43
|
-
/// - <project_root>/../server/actions
|
|
44
|
-
/// - /app/actions
|
|
45
|
-
/// - ./actions
|
|
46
|
-
fn find_actions_dir(project_root: &PathBuf) -> Option<PathBuf> {
|
|
47
|
-
let candidates = [
|
|
48
|
-
project_root.join("server").join("actions"),
|
|
49
|
-
project_root.join("actions"),
|
|
50
|
-
project_root.join("..").join("server").join("actions"),
|
|
51
|
-
PathBuf::from("/app").join("actions"),
|
|
52
|
-
PathBuf::from("actions"),
|
|
53
|
-
];
|
|
54
|
-
|
|
55
|
-
for p in &candidates {
|
|
56
|
-
if p.exists() && p.is_dir() {
|
|
57
|
-
return Some(p.clone());
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
None
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/// Injects a synchronous `t.fetch(url, opts?)` function into the Boa `Context`.
|
|
65
|
-
///
|
|
66
|
-
/// Implementation details:
|
|
67
|
-
/// - Converts JS opts → `serde_json::Value` (owned) using `to_json`.
|
|
68
|
-
/// - Executes reqwest blocking client inside `tokio::task::block_in_place` to avoid blocking async runtime.
|
|
69
|
-
/// - Returns `{ ok: bool, status?: number, body?: string, error?: string }`.
|
|
70
43
|
fn inject_t_fetch(ctx: &mut Context) {
|
|
71
|
-
// Native function (Boa 0.20) using from_fn_ptr
|
|
72
44
|
let t_fetch_native = NativeFunction::from_fn_ptr(|_this, args, ctx| {
|
|
73
|
-
// Extract URL (owned string)
|
|
74
45
|
let url = args
|
|
75
46
|
.get(0)
|
|
76
47
|
.and_then(|v| v.to_string(ctx).ok())
|
|
77
48
|
.map(|s| s.to_std_string_escaped())
|
|
78
49
|
.unwrap_or_default();
|
|
79
50
|
|
|
80
|
-
// Extract opts -> convert to serde_json::Value (owned)
|
|
81
51
|
let opts_js = args.get(1).cloned().unwrap_or(JsValue::undefined());
|
|
82
|
-
let opts_json: Value =
|
|
83
|
-
Ok(v) => v,
|
|
84
|
-
Err(_) => Value::Object(serde_json::Map::new()),
|
|
85
|
-
};
|
|
52
|
+
let opts_json: Value = opts_js.to_json(ctx).unwrap_or(Value::Null);
|
|
86
53
|
|
|
87
|
-
// Pull method, body, headers into owned Rust values
|
|
88
54
|
let method = opts_json
|
|
89
55
|
.get("method")
|
|
90
56
|
.and_then(|m| m.as_str())
|
|
91
|
-
.
|
|
92
|
-
.
|
|
57
|
+
.unwrap_or("GET")
|
|
58
|
+
.to_string();
|
|
93
59
|
|
|
94
|
-
let body_opt =
|
|
95
|
-
Some(Value::String(s)) => Some(s.clone()),
|
|
96
|
-
Some(other) => Some(other.to_string()),
|
|
97
|
-
None => None,
|
|
98
|
-
};
|
|
60
|
+
let body_opt = opts_json.get("body").cloned();
|
|
99
61
|
|
|
100
|
-
|
|
101
|
-
let mut header_pairs: Vec<(String, String)> = Vec::new();
|
|
62
|
+
let mut headers = HeaderMap::new();
|
|
102
63
|
if let Some(Value::Object(map)) = opts_json.get("headers") {
|
|
103
|
-
for (k, v) in map
|
|
104
|
-
let
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
64
|
+
for (k, v) in map {
|
|
65
|
+
if let Ok(name) = HeaderName::from_bytes(k.as_bytes()) {
|
|
66
|
+
if let Ok(val) = HeaderValue::from_str(&v.to_string()) {
|
|
67
|
+
headers.insert(name, val);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
109
70
|
}
|
|
110
71
|
}
|
|
111
72
|
|
|
112
|
-
|
|
113
|
-
let out_json = task::block_in_place(move || {
|
|
73
|
+
let out = task::block_in_place(move || {
|
|
114
74
|
let client = Client::new();
|
|
75
|
+
let mut req = client.request(method.parse().unwrap(), &url);
|
|
115
76
|
|
|
116
|
-
|
|
117
|
-
let mut req = client.request(method_parsed, &url);
|
|
118
|
-
|
|
119
|
-
if !header_pairs.is_empty() {
|
|
120
|
-
let mut headers = HeaderMap::new();
|
|
121
|
-
for (k, v) in header_pairs.into_iter() {
|
|
122
|
-
if let (Ok(name), Ok(val)) =
|
|
123
|
-
(HeaderName::from_bytes(k.as_bytes()), HeaderValue::from_str(&v))
|
|
124
|
-
{
|
|
125
|
-
headers.insert(name, val);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
req = req.headers(headers);
|
|
129
|
-
}
|
|
77
|
+
req = req.headers(headers);
|
|
130
78
|
|
|
131
79
|
if let Some(body) = body_opt {
|
|
132
|
-
req = req.body(body);
|
|
80
|
+
req = req.body(body.to_string());
|
|
133
81
|
}
|
|
134
82
|
|
|
135
83
|
match req.send() {
|
|
136
84
|
Ok(resp) => {
|
|
137
85
|
let status = resp.status().as_u16();
|
|
138
86
|
let text = resp.text().unwrap_or_default();
|
|
139
|
-
serde_json::json!({
|
|
140
|
-
"ok": true,
|
|
141
|
-
"status": status,
|
|
142
|
-
"body": text
|
|
143
|
-
})
|
|
87
|
+
serde_json::json!({ "ok": true, "status": status, "body": text })
|
|
144
88
|
}
|
|
145
|
-
Err(e) => serde_json::json!({
|
|
146
|
-
"ok": false,
|
|
147
|
-
"error": e.to_string()
|
|
148
|
-
}),
|
|
89
|
+
Err(e) => serde_json::json!({ "ok": false, "error": e.to_string() }),
|
|
149
90
|
}
|
|
150
91
|
});
|
|
151
92
|
|
|
152
|
-
|
|
153
|
-
Ok(JsValue::from_json(&out_json, ctx).unwrap_or(JsValue::undefined()))
|
|
93
|
+
Ok(JsValue::from_json(&out, ctx).unwrap())
|
|
154
94
|
});
|
|
155
95
|
|
|
156
|
-
// Convert native function to JS function object (requires Realm)
|
|
157
96
|
let realm = ctx.realm();
|
|
158
97
|
let t_fetch_js_fn = t_fetch_native.to_js_function(realm);
|
|
159
98
|
|
|
160
|
-
// Build `t` object with `.fetch`
|
|
161
99
|
let t_obj = ObjectInitializer::new(ctx)
|
|
162
100
|
.property(js_string!("fetch"), t_fetch_js_fn, Attribute::all())
|
|
163
101
|
.build();
|
|
164
102
|
|
|
165
103
|
ctx.global_object()
|
|
166
104
|
.set(js_string!("t"), JsValue::from(t_obj), false, ctx)
|
|
167
|
-
.
|
|
105
|
+
.unwrap();
|
|
168
106
|
}
|
|
169
107
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
+
}
|
|
174
130
|
}
|
|
175
131
|
|
|
176
|
-
|
|
177
|
-
|
|
132
|
+
|
|
133
|
+
fn resolve_actions_dir(project_root: &PathBuf) -> PathBuf {
|
|
134
|
+
let exe = std::env::current_exe().unwrap();
|
|
135
|
+
let exe_dir = exe.parent().unwrap();
|
|
136
|
+
let is_prod = exe_dir.to_string_lossy().contains("/app")
|
|
137
|
+
|| exe_dir.ends_with("release");
|
|
138
|
+
|
|
139
|
+
if is_prod {
|
|
140
|
+
PathBuf::from("/app/actions")
|
|
141
|
+
} else {
|
|
142
|
+
project_root.join("server").join("actions")
|
|
143
|
+
}
|
|
178
144
|
}
|
|
179
145
|
|
|
180
|
-
|
|
181
|
-
async fn
|
|
146
|
+
|
|
147
|
+
async fn dynamic_handler(
|
|
182
148
|
State(state): State<AppState>,
|
|
183
149
|
req: Request<Body>,
|
|
184
150
|
) -> impl IntoResponse {
|
|
185
151
|
let method = req.method().as_str().to_uppercase();
|
|
186
|
-
let path = req.uri().path();
|
|
152
|
+
let path = req.uri().path().to_string();
|
|
187
153
|
let key = format!("{}:{}", method, path);
|
|
188
154
|
|
|
189
155
|
let body_bytes = match to_bytes(req.into_body(), usize::MAX).await {
|
|
190
|
-
Ok(
|
|
156
|
+
Ok(v) => v,
|
|
191
157
|
Err(_) => return (StatusCode::BAD_REQUEST, "Failed to read body").into_response(),
|
|
192
158
|
};
|
|
193
159
|
let body_str = String::from_utf8_lossy(&body_bytes).to_string();
|
|
194
160
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if action_name.is_empty() {
|
|
200
|
-
return (StatusCode::INTERNAL_SERVER_ERROR, "Invalid action name").into_response();
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Resolve actions directory robustly
|
|
204
|
-
let actions_dir = find_actions_dir(&state.project_root);
|
|
205
|
-
let actions_dir = match actions_dir {
|
|
206
|
-
Some(p) => p,
|
|
207
|
-
None => {
|
|
208
|
-
return (
|
|
209
|
-
StatusCode::INTERNAL_SERVER_ERROR,
|
|
210
|
-
format!("Actions directory not found (checked multiple locations)"),
|
|
211
|
-
)
|
|
212
|
-
.into_response();
|
|
213
|
-
}
|
|
214
|
-
};
|
|
161
|
+
let route = match state.routes.get(&key) {
|
|
162
|
+
Some(r) => r,
|
|
163
|
+
None => return (StatusCode::NOT_FOUND, "Not Found").into_response(),
|
|
164
|
+
};
|
|
215
165
|
|
|
216
|
-
|
|
166
|
+
if route.r#type != "action" {
|
|
167
|
+
if let Some(s) = route.value.as_str() {
|
|
168
|
+
return s.to_string().into_response();
|
|
169
|
+
}
|
|
170
|
+
return Json(route.value.clone()).into_response();
|
|
171
|
+
}
|
|
217
172
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
173
|
+
let action_name = route.value.as_str().unwrap().trim().to_string();
|
|
174
|
+
if action_name.is_empty() {
|
|
175
|
+
return (
|
|
176
|
+
StatusCode::INTERNAL_SERVER_ERROR,
|
|
177
|
+
"Invalid action name",
|
|
178
|
+
)
|
|
179
|
+
.into_response();
|
|
180
|
+
}
|
|
225
181
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
Err(e) => {
|
|
229
|
-
return (
|
|
230
|
-
StatusCode::INTERNAL_SERVER_ERROR,
|
|
231
|
-
format!("Failed reading action bundle: {}", e),
|
|
232
|
-
)
|
|
233
|
-
.into_response();
|
|
234
|
-
}
|
|
235
|
-
};
|
|
182
|
+
let actions_dir = resolve_actions_dir(&state.project_root);
|
|
183
|
+
let action_path = actions_dir.join(format!("{}.jsbundle", action_name));
|
|
236
184
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
// Injected script: sets process.env and __titan_req and invokes action function.
|
|
245
|
-
let injected = format!(
|
|
246
|
-
r#"
|
|
247
|
-
globalThis.process = {{ env: {} }};
|
|
248
|
-
const __titan_req = {};
|
|
249
|
-
{};
|
|
250
|
-
{}(__titan_req);
|
|
251
|
-
"#,
|
|
252
|
-
env_json.to_string(),
|
|
253
|
-
body_str,
|
|
254
|
-
js_code,
|
|
255
|
-
action_name
|
|
256
|
-
);
|
|
257
|
-
|
|
258
|
-
let mut ctx = Context::default();
|
|
259
|
-
inject_t_fetch(&mut ctx);
|
|
260
|
-
|
|
261
|
-
let result = match ctx.eval(Source::from_bytes(&injected)) {
|
|
262
|
-
Ok(v) => v,
|
|
263
|
-
Err(e) => return Json(json_error(e.to_string())).into_response(),
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
let result_json: Value = match result.to_json(&mut ctx) {
|
|
267
|
-
Ok(v) => v,
|
|
268
|
-
Err(e) => json_error(e.to_string()),
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
return Json(result_json).into_response();
|
|
272
|
-
}
|
|
185
|
+
if !action_path.exists() {
|
|
186
|
+
return (
|
|
187
|
+
StatusCode::NOT_FOUND,
|
|
188
|
+
format!("Action bundle not found: {:?}", action_path),
|
|
189
|
+
)
|
|
190
|
+
.into_response();
|
|
191
|
+
}
|
|
273
192
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
193
|
+
let js_code = match fs::read_to_string(&action_path) {
|
|
194
|
+
Ok(v) => v,
|
|
195
|
+
Err(e) => {
|
|
196
|
+
return (
|
|
197
|
+
StatusCode::INTERNAL_SERVER_ERROR,
|
|
198
|
+
format!("Failed reading bundle: {}", e),
|
|
199
|
+
)
|
|
200
|
+
.into_response();
|
|
281
201
|
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
let mut env_map = serde_json::Map::new();
|
|
205
|
+
for (k, v) in std::env::vars() {
|
|
206
|
+
env_map.insert(k, Value::String(v));
|
|
282
207
|
}
|
|
208
|
+
let env_json = Value::Object(env_map);
|
|
209
|
+
|
|
210
|
+
let injected = format!(
|
|
211
|
+
r#"
|
|
212
|
+
globalThis.process = {{ env: {} }};
|
|
213
|
+
const __titan_req = {};
|
|
214
|
+
{};
|
|
215
|
+
{}(__titan_req);
|
|
216
|
+
"#,
|
|
217
|
+
env_json.to_string(),
|
|
218
|
+
body_str,
|
|
219
|
+
js_code,
|
|
220
|
+
action_name
|
|
221
|
+
);
|
|
283
222
|
|
|
284
|
-
|
|
285
|
-
|
|
223
|
+
let mut ctx = Context::default();
|
|
224
|
+
inject_t_fetch(&mut ctx);
|
|
286
225
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
226
|
+
let result = match ctx.eval(Source::from_bytes(&injected)) {
|
|
227
|
+
Ok(v) => v,
|
|
228
|
+
Err(e) => return Json(serde_json::json!({ "error": e.to_string() })).into_response(),
|
|
229
|
+
};
|
|
290
230
|
|
|
291
|
-
|
|
231
|
+
let result_json = result.to_json(&mut ctx).unwrap_or(Value::Null);
|
|
232
|
+
Json(result_json).into_response()
|
|
233
|
+
}
|
|
292
234
|
|
|
293
235
|
#[tokio::main]
|
|
294
236
|
async fn main() -> Result<()> {
|
|
295
237
|
dotenvy::dotenv().ok();
|
|
296
238
|
|
|
297
|
-
|
|
298
|
-
let raw = fs::read_to_string("./routes.json").unwrap_or_else(|_| "{}".to_string());
|
|
239
|
+
let raw = fs::read_to_string("./routes.json").unwrap_or("{}".into());
|
|
299
240
|
let json: Value = serde_json::from_str(&raw).unwrap_or_default();
|
|
300
241
|
|
|
301
242
|
let port = json["__config"]["port"].as_u64().unwrap_or(3000);
|
|
302
|
-
let
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
// Project root — heuristics: try current_dir()
|
|
306
|
-
let project_root = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
|
307
|
-
|
|
308
|
-
// Debug logging — show where we looked for actions
|
|
309
|
-
eprintln!("DEBUG runtime cwd: {:?}", std::env::current_dir());
|
|
310
|
-
eprintln!("DEBUG project_root: {:?}", project_root);
|
|
311
|
-
eprintln!(
|
|
312
|
-
"DEBUG found actions dir (if any): {:?}",
|
|
313
|
-
find_actions_dir(&project_root)
|
|
314
|
-
);
|
|
243
|
+
let routes_map: HashMap<String, RouteVal> =
|
|
244
|
+
serde_json::from_value(json["routes"].clone()).unwrap_or_default();
|
|
315
245
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
246
|
+
let project_root = detect_project_root();
|
|
247
|
+
|
|
248
|
+
let state = AppState {
|
|
249
|
+
routes: Arc::new(routes_map),
|
|
250
|
+
project_root,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
|
|
320
254
|
|
|
321
255
|
let app = Router::new()
|
|
322
|
-
.route("/", any(
|
|
323
|
-
.fallback(any(
|
|
256
|
+
.route("/", any(dynamic_handler))
|
|
257
|
+
.fallback(any(dynamic_handler))
|
|
324
258
|
.with_state(state);
|
|
325
259
|
|
|
326
260
|
let listener = TcpListener::bind(format!("0.0.0.0:{}", port)).await?;
|
|
327
|
-
|
|
328
|
-
// Banner (yellow-orange) and server info
|
|
329
|
-
println!("\n\x1b[38;5;208m████████╗██╗████████╗ █████╗ ███╗ ██╗");
|
|
330
|
-
println!("╚══██╔══╝██║╚══██╔══╝██╔══██╗████╗ ██║");
|
|
331
|
-
println!(" ██║ ██║ ██║ ███████║██╔██╗ ██║");
|
|
332
|
-
println!(" ██║ ██║ ██║ ██╔══██║██║╚██╗██║");
|
|
333
|
-
println!(" ██║ ██║ ██║ ██║ ██║██║ ╚████║");
|
|
334
|
-
println!(" ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝\x1b[0m\n");
|
|
335
|
-
println!("\x1b[38;5;39mTitan server running at:\x1b[0m http://localhost:{}", port);
|
|
261
|
+
println!("Titan server running on {}", port);
|
|
336
262
|
|
|
337
263
|
axum::serve(listener, app).await?;
|
|
338
264
|
Ok(())
|