@ezetgalaxy/titan 25.14.0 → 25.14.4
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 +24 -33
- package/package.json +1 -1
- package/templates/server/src/main.rs +154 -218
package/index.js
CHANGED
|
@@ -187,51 +187,41 @@ function buildProd() {
|
|
|
187
187
|
|
|
188
188
|
const root = process.cwd();
|
|
189
189
|
const appJs = path.join(root, "app", "app.js");
|
|
190
|
-
const bundler = path.join(root, "titan", "bundle.js");
|
|
191
190
|
const serverDir = path.join(root, "server");
|
|
192
|
-
const builtActionsDir = path.join(root, "server", "actions");
|
|
193
191
|
const actionsOut = path.join(serverDir, "actions");
|
|
194
|
-
|
|
192
|
+
|
|
193
|
+
// BASIC CHECKS
|
|
195
194
|
if (!fs.existsSync(appJs)) {
|
|
196
195
|
console.log(red("ERROR: app/app.js not found."));
|
|
197
196
|
process.exit(1);
|
|
198
197
|
}
|
|
199
198
|
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// 1) Generate routes.json and action_map.json
|
|
207
|
-
console.log(cyan("→ Generating Titan metadata..."));
|
|
199
|
+
// ----------------------------------------------------
|
|
200
|
+
// 1) BUILD METADATA + BUNDLE ACTIONS (ONE TIME ONLY)
|
|
201
|
+
// ----------------------------------------------------
|
|
202
|
+
console.log(cyan("→ Building Titan metadata + bundling actions..."));
|
|
208
203
|
execSync("node app/app.js --build", { stdio: "inherit" });
|
|
209
204
|
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
for (const file of fs.readdirSync(builtActionsDir)) {
|
|
220
|
-
if (file.endsWith(".jsbundle")) {
|
|
221
|
-
console.log(cyan(`→ Copying ${file}`));
|
|
222
|
-
fs.copyFileSync(
|
|
223
|
-
path.join(builtActionsDir, file),
|
|
224
|
-
path.join(actionsOut, file)
|
|
225
|
-
);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
} else {
|
|
229
|
-
console.log(yellow("Warning: No actions bundled."));
|
|
205
|
+
// ensure actions directory exists
|
|
206
|
+
fs.mkdirSync(actionsOut, { recursive: true });
|
|
207
|
+
|
|
208
|
+
// verify bundled actions exist
|
|
209
|
+
const bundles = fs.readdirSync(actionsOut).filter(f => f.endsWith(".jsbundle"));
|
|
210
|
+
if (bundles.length === 0) {
|
|
211
|
+
console.log(red("ERROR: No actions bundled."));
|
|
212
|
+
console.log(red("Make sure your DSL outputs to server/actions."));
|
|
213
|
+
process.exit(1);
|
|
230
214
|
}
|
|
231
215
|
|
|
232
|
-
|
|
216
|
+
bundles.forEach(file => {
|
|
217
|
+
console.log(cyan(`→ Found action bundle: ${file}`));
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
console.log(green("✔ Actions ready in server/actions"));
|
|
233
221
|
|
|
234
|
-
//
|
|
222
|
+
// ----------------------------------------------------
|
|
223
|
+
// 2) BUILD RUST BINARY
|
|
224
|
+
// ----------------------------------------------------
|
|
235
225
|
console.log(cyan("→ Building Rust release binary..."));
|
|
236
226
|
execSync("cargo build --release", {
|
|
237
227
|
cwd: serverDir,
|
|
@@ -242,6 +232,7 @@ function buildProd() {
|
|
|
242
232
|
}
|
|
243
233
|
|
|
244
234
|
|
|
235
|
+
|
|
245
236
|
/* START PRODUCTION BINARY */
|
|
246
237
|
function startProd() {
|
|
247
238
|
const isWin = process.platform === "win32";
|
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,239 @@ 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() -> PathBuf {
|
|
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")
|
|
178
153
|
}
|
|
179
154
|
|
|
180
|
-
|
|
181
|
-
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
async fn dynamic_handler(
|
|
182
158
|
State(state): State<AppState>,
|
|
183
159
|
req: Request<Body>,
|
|
184
160
|
) -> impl IntoResponse {
|
|
185
161
|
let method = req.method().as_str().to_uppercase();
|
|
186
|
-
let path = req.uri().path();
|
|
162
|
+
let path = req.uri().path().to_string();
|
|
187
163
|
let key = format!("{}:{}", method, path);
|
|
188
164
|
|
|
189
165
|
let body_bytes = match to_bytes(req.into_body(), usize::MAX).await {
|
|
190
|
-
Ok(
|
|
166
|
+
Ok(v) => v,
|
|
191
167
|
Err(_) => return (StatusCode::BAD_REQUEST, "Failed to read body").into_response(),
|
|
192
168
|
};
|
|
193
169
|
let body_str = String::from_utf8_lossy(&body_bytes).to_string();
|
|
194
170
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if action_name.is_empty() {
|
|
200
|
-
return (StatusCode::INTERNAL_SERVER_ERROR, "Invalid action name").into_response();
|
|
201
|
-
}
|
|
171
|
+
let route = match state.routes.get(&key) {
|
|
172
|
+
Some(r) => r,
|
|
173
|
+
None => return (StatusCode::NOT_FOUND, "Not Found").into_response(),
|
|
174
|
+
};
|
|
202
175
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
StatusCode::INTERNAL_SERVER_ERROR,
|
|
210
|
-
format!("Actions directory not found (checked multiple locations)"),
|
|
211
|
-
)
|
|
212
|
-
.into_response();
|
|
213
|
-
}
|
|
214
|
-
};
|
|
176
|
+
if route.r#type != "action" {
|
|
177
|
+
if let Some(s) = route.value.as_str() {
|
|
178
|
+
return s.to_string().into_response();
|
|
179
|
+
}
|
|
180
|
+
return Json(route.value.clone()).into_response();
|
|
181
|
+
}
|
|
215
182
|
|
|
216
|
-
|
|
183
|
+
let action_name = route.value.as_str().unwrap().trim().to_string();
|
|
184
|
+
if action_name.is_empty() {
|
|
185
|
+
return (
|
|
186
|
+
StatusCode::INTERNAL_SERVER_ERROR,
|
|
187
|
+
"Invalid action name",
|
|
188
|
+
)
|
|
189
|
+
.into_response();
|
|
190
|
+
}
|
|
217
191
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
StatusCode::NOT_FOUND,
|
|
221
|
-
format!("Action bundle not found: {:?}", action_path),
|
|
222
|
-
)
|
|
223
|
-
.into_response();
|
|
224
|
-
}
|
|
192
|
+
let actions_dir = resolve_actions_dir(&state.project_root);
|
|
193
|
+
let action_path = actions_dir.join(format!("{}.jsbundle", action_name));
|
|
225
194
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
.into_response();
|
|
234
|
-
}
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
// Build env object
|
|
238
|
-
let mut env_map = serde_json::Map::new();
|
|
239
|
-
for (k, v) in std::env::vars() {
|
|
240
|
-
env_map.insert(k, Value::String(v));
|
|
241
|
-
}
|
|
242
|
-
let env_json = Value::Object(env_map);
|
|
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
|
-
}
|
|
195
|
+
if !action_path.exists() {
|
|
196
|
+
return (
|
|
197
|
+
StatusCode::NOT_FOUND,
|
|
198
|
+
format!("Action bundle not found: {:?}", action_path),
|
|
199
|
+
)
|
|
200
|
+
.into_response();
|
|
201
|
+
}
|
|
273
202
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
203
|
+
let js_code = match fs::read_to_string(&action_path) {
|
|
204
|
+
Ok(v) => v,
|
|
205
|
+
Err(e) => {
|
|
206
|
+
return (
|
|
207
|
+
StatusCode::INTERNAL_SERVER_ERROR,
|
|
208
|
+
format!("Failed reading bundle: {}", e),
|
|
209
|
+
)
|
|
210
|
+
.into_response();
|
|
281
211
|
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
let mut env_map = serde_json::Map::new();
|
|
215
|
+
for (k, v) in std::env::vars() {
|
|
216
|
+
env_map.insert(k, Value::String(v));
|
|
282
217
|
}
|
|
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
|
+
);
|
|
283
232
|
|
|
284
|
-
|
|
285
|
-
|
|
233
|
+
let mut ctx = Context::default();
|
|
234
|
+
inject_t_fetch(&mut ctx);
|
|
286
235
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
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
|
+
};
|
|
290
240
|
|
|
291
|
-
|
|
241
|
+
let result_json = result.to_json(&mut ctx).unwrap_or(Value::Null);
|
|
242
|
+
Json(result_json).into_response()
|
|
243
|
+
}
|
|
292
244
|
|
|
293
245
|
#[tokio::main]
|
|
294
246
|
async fn main() -> Result<()> {
|
|
295
247
|
dotenvy::dotenv().ok();
|
|
296
248
|
|
|
297
|
-
|
|
298
|
-
let raw = fs::read_to_string("./routes.json").unwrap_or_else(|_| "{}".to_string());
|
|
249
|
+
let raw = fs::read_to_string("./routes.json").unwrap_or("{}".into());
|
|
299
250
|
let json: Value = serde_json::from_str(&raw).unwrap_or_default();
|
|
300
251
|
|
|
301
252
|
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
|
-
);
|
|
253
|
+
let routes_map: HashMap<String, RouteVal> =
|
|
254
|
+
serde_json::from_value(json["routes"].clone()).unwrap_or_default();
|
|
315
255
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
256
|
+
let project_root = detect_project_root();
|
|
257
|
+
|
|
258
|
+
let state = AppState {
|
|
259
|
+
routes: Arc::new(routes_map),
|
|
260
|
+
project_root,
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
|
|
320
264
|
|
|
321
265
|
let app = Router::new()
|
|
322
|
-
.route("/", any(
|
|
323
|
-
.fallback(any(
|
|
266
|
+
.route("/", any(dynamic_handler))
|
|
267
|
+
.fallback(any(dynamic_handler))
|
|
324
268
|
.with_state(state);
|
|
325
269
|
|
|
326
270
|
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);
|
|
271
|
+
println!("Titan server running on {}", port);
|
|
336
272
|
|
|
337
273
|
axum::serve(listener, app).await?;
|
|
338
274
|
Ok(())
|