@ezetgalaxy/titan 25.13.3 → 25.13.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/package.json +1 -1
- package/templates/Dockerfile +13 -21
- package/templates/server/src/main.rs +95 -77
package/package.json
CHANGED
package/templates/Dockerfile
CHANGED
|
@@ -3,57 +3,49 @@
|
|
|
3
3
|
# ================================================================
|
|
4
4
|
FROM rust:1.91.1 AS builder
|
|
5
5
|
|
|
6
|
-
# Install Node for Titan CLI
|
|
6
|
+
# Install Node for Titan CLI + bundler
|
|
7
7
|
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
|
8
8
|
&& apt-get install -y nodejs
|
|
9
9
|
|
|
10
|
-
# Install Titan CLI
|
|
11
|
-
RUN npm install -g @ezetgalaxy/titan
|
|
10
|
+
# Install Titan CLI (latest)
|
|
11
|
+
RUN npm install -g @ezetgalaxy/titan@latest
|
|
12
12
|
|
|
13
13
|
WORKDIR /app
|
|
14
14
|
|
|
15
|
-
# Copy project files
|
|
15
|
+
# Copy project files
|
|
16
16
|
COPY . .
|
|
17
17
|
|
|
18
|
-
# Install JS dependencies (for
|
|
18
|
+
# Install JS dependencies (needed for Titan DSL + bundler)
|
|
19
19
|
RUN npm install
|
|
20
20
|
|
|
21
|
-
# Build Titan metadata
|
|
21
|
+
# Build Titan metadata + bundle JS actions
|
|
22
22
|
RUN tit build
|
|
23
23
|
|
|
24
|
-
# Build
|
|
24
|
+
# Build Rust binary
|
|
25
25
|
RUN cd server && cargo build --release
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
# ================================================================
|
|
30
|
-
# STAGE 2 — Runtime Image
|
|
30
|
+
# STAGE 2 — Runtime Image (Lightweight)
|
|
31
31
|
# ================================================================
|
|
32
32
|
FROM debian:stable-slim
|
|
33
33
|
|
|
34
34
|
WORKDIR /app
|
|
35
35
|
|
|
36
|
-
#
|
|
37
|
-
# Copy the Rust binary
|
|
38
|
-
# -------------------------------------------------------------
|
|
36
|
+
# Copy Rust binary from builder stage
|
|
39
37
|
COPY --from=builder /app/server/target/release/server ./titan-server
|
|
40
38
|
|
|
41
|
-
#
|
|
42
|
-
# Copy Titan routing metadata (REQUIRED)
|
|
43
|
-
# -------------------------------------------------------------
|
|
39
|
+
# Copy Titan routing metadata
|
|
44
40
|
COPY --from=builder /app/server/routes.json ./routes.json
|
|
45
41
|
COPY --from=builder /app/server/action_map.json ./action_map.json
|
|
46
42
|
|
|
47
|
-
#
|
|
48
|
-
# Copy Titan JS bundles directory (REQUIRED)
|
|
49
|
-
# -------------------------------------------------------------
|
|
43
|
+
# Copy Titan JS bundles
|
|
50
44
|
RUN mkdir -p /app/actions
|
|
51
45
|
COPY --from=builder /app/server/actions /app/actions
|
|
52
46
|
|
|
53
|
-
|
|
54
|
-
# -------------------------------------------------------------
|
|
55
|
-
# Runtime configuration
|
|
56
|
-
# -------------------------------------------------------------
|
|
47
|
+
# Expose Titan port
|
|
57
48
|
EXPOSE 3000
|
|
58
49
|
|
|
50
|
+
# Start Titan
|
|
59
51
|
CMD ["./titan-server"]
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// src/main.rs
|
|
2
|
-
use std::{collections::HashMap, fs,
|
|
1
|
+
// server/src/main.rs
|
|
2
|
+
use std::{collections::HashMap, fs, path::PathBuf, sync::Arc};
|
|
3
3
|
|
|
4
4
|
use anyhow::Result;
|
|
5
5
|
use axum::{
|
|
6
|
-
body::{
|
|
6
|
+
body::{to_bytes, Body},
|
|
7
7
|
extract::State,
|
|
8
8
|
http::{Request, StatusCode},
|
|
9
9
|
response::{IntoResponse, Json},
|
|
@@ -11,23 +11,18 @@ use axum::{
|
|
|
11
11
|
Router,
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
use boa_engine::{
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
};
|
|
20
|
-
use boa_engine::js_string;
|
|
14
|
+
use boa_engine::{object::ObjectInitializer, Context, JsValue, Source};
|
|
15
|
+
use boa_engine::{js_string, native_function::NativeFunction, property::Attribute};
|
|
16
|
+
|
|
17
|
+
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
|
|
18
|
+
use reqwest::blocking::Client;
|
|
21
19
|
|
|
22
20
|
use serde::Deserialize;
|
|
23
21
|
use serde_json::Value;
|
|
24
22
|
use tokio::net::TcpListener;
|
|
25
23
|
use tokio::task;
|
|
26
24
|
|
|
27
|
-
|
|
28
|
-
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
|
|
29
|
-
|
|
30
|
-
/// Route configuration entry parsed from routes.json
|
|
25
|
+
/// Route configuration (loaded from routes.json)
|
|
31
26
|
#[derive(Debug, Deserialize)]
|
|
32
27
|
struct RouteVal {
|
|
33
28
|
r#type: String,
|
|
@@ -40,24 +35,56 @@ struct AppState {
|
|
|
40
35
|
project_root: PathBuf,
|
|
41
36
|
}
|
|
42
37
|
|
|
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
|
+
}
|
|
43
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 }`.
|
|
44
70
|
fn inject_t_fetch(ctx: &mut Context) {
|
|
45
|
-
//
|
|
71
|
+
// Native function (Boa 0.20) using from_fn_ptr
|
|
46
72
|
let t_fetch_native = NativeFunction::from_fn_ptr(|_this, args, ctx| {
|
|
47
|
-
// Extract
|
|
73
|
+
// Extract URL (owned string)
|
|
48
74
|
let url = args
|
|
49
75
|
.get(0)
|
|
50
|
-
.and_then(|v| v.
|
|
76
|
+
.and_then(|v| v.to_string(ctx).ok())
|
|
77
|
+
.map(|s| s.to_std_string_escaped())
|
|
51
78
|
.unwrap_or_default();
|
|
52
79
|
|
|
53
|
-
// opts
|
|
80
|
+
// Extract opts -> convert to serde_json::Value (owned)
|
|
54
81
|
let opts_js = args.get(1).cloned().unwrap_or(JsValue::undefined());
|
|
55
82
|
let opts_json: Value = match opts_js.to_json(ctx) {
|
|
56
83
|
Ok(v) => v,
|
|
57
84
|
Err(_) => Value::Object(serde_json::Map::new()),
|
|
58
85
|
};
|
|
59
86
|
|
|
60
|
-
//
|
|
87
|
+
// Pull method, body, headers into owned Rust values
|
|
61
88
|
let method = opts_json
|
|
62
89
|
.get("method")
|
|
63
90
|
.and_then(|m| m.as_str())
|
|
@@ -70,7 +97,7 @@ fn inject_t_fetch(ctx: &mut Context) {
|
|
|
70
97
|
None => None,
|
|
71
98
|
};
|
|
72
99
|
|
|
73
|
-
//
|
|
100
|
+
// headers as Vec<(String,String)>
|
|
74
101
|
let mut header_pairs: Vec<(String, String)> = Vec::new();
|
|
75
102
|
if let Some(Value::Object(map)) = opts_json.get("headers") {
|
|
76
103
|
for (k, v) in map.iter() {
|
|
@@ -82,16 +109,13 @@ fn inject_t_fetch(ctx: &mut Context) {
|
|
|
82
109
|
}
|
|
83
110
|
}
|
|
84
111
|
|
|
85
|
-
// Perform blocking HTTP request inside block_in_place
|
|
112
|
+
// Perform the blocking HTTP request inside block_in_place to avoid runtime panic
|
|
86
113
|
let out_json = task::block_in_place(move || {
|
|
87
|
-
// Create blocking client
|
|
88
114
|
let client = Client::new();
|
|
89
115
|
|
|
90
|
-
// Build request
|
|
91
116
|
let method_parsed = method.parse().unwrap_or(reqwest::Method::GET);
|
|
92
117
|
let mut req = client.request(method_parsed, &url);
|
|
93
118
|
|
|
94
|
-
// Attach headers
|
|
95
119
|
if !header_pairs.is_empty() {
|
|
96
120
|
let mut headers = HeaderMap::new();
|
|
97
121
|
for (k, v) in header_pairs.into_iter() {
|
|
@@ -108,11 +132,9 @@ fn inject_t_fetch(ctx: &mut Context) {
|
|
|
108
132
|
req = req.body(body);
|
|
109
133
|
}
|
|
110
134
|
|
|
111
|
-
// Send request
|
|
112
135
|
match req.send() {
|
|
113
136
|
Ok(resp) => {
|
|
114
137
|
let status = resp.status().as_u16();
|
|
115
|
-
// Try to read text, fallback to empty string on error
|
|
116
138
|
let text = resp.text().unwrap_or_default();
|
|
117
139
|
serde_json::json!({
|
|
118
140
|
"ok": true,
|
|
@@ -120,44 +142,42 @@ fn inject_t_fetch(ctx: &mut Context) {
|
|
|
120
142
|
"body": text
|
|
121
143
|
})
|
|
122
144
|
}
|
|
123
|
-
Err(e) => {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
})
|
|
128
|
-
}
|
|
145
|
+
Err(e) => serde_json::json!({
|
|
146
|
+
"ok": false,
|
|
147
|
+
"error": e.to_string()
|
|
148
|
+
}),
|
|
129
149
|
}
|
|
130
150
|
});
|
|
131
151
|
|
|
132
|
-
// Convert serde_json::Value -> JsValue
|
|
152
|
+
// Convert serde_json::Value -> JsValue
|
|
133
153
|
Ok(JsValue::from_json(&out_json, ctx).unwrap_or(JsValue::undefined()))
|
|
134
154
|
});
|
|
135
155
|
|
|
136
|
-
// Convert
|
|
156
|
+
// Convert native function to JS function object (requires Realm)
|
|
137
157
|
let realm = ctx.realm();
|
|
138
158
|
let t_fetch_js_fn = t_fetch_native.to_js_function(realm);
|
|
139
159
|
|
|
140
|
-
// Build `t` object with `.fetch`
|
|
160
|
+
// Build `t` object with `.fetch`
|
|
141
161
|
let t_obj = ObjectInitializer::new(ctx)
|
|
142
162
|
.property(js_string!("fetch"), t_fetch_js_fn, Attribute::all())
|
|
143
163
|
.build();
|
|
144
164
|
|
|
145
|
-
// Attach to globalThis.t
|
|
146
165
|
ctx.global_object()
|
|
147
166
|
.set(js_string!("t"), JsValue::from(t_obj), false, ctx)
|
|
148
|
-
.
|
|
167
|
+
.expect("set global t");
|
|
149
168
|
}
|
|
150
169
|
|
|
151
|
-
//
|
|
170
|
+
// Root/dynamic handlers -----------------------------------------------------
|
|
152
171
|
|
|
153
172
|
async fn root_route(state: State<AppState>, req: Request<Body>) -> impl IntoResponse {
|
|
154
173
|
dynamic_handler_inner(state, req).await
|
|
155
174
|
}
|
|
175
|
+
|
|
156
176
|
async fn dynamic_route(state: State<AppState>, req: Request<Body>) -> impl IntoResponse {
|
|
157
177
|
dynamic_handler_inner(state, req).await
|
|
158
178
|
}
|
|
159
179
|
|
|
160
|
-
/// Main handler
|
|
180
|
+
/// Main handler: looks up routes.json and executes action bundles using Boa.
|
|
161
181
|
async fn dynamic_handler_inner(
|
|
162
182
|
State(state): State<AppState>,
|
|
163
183
|
req: Request<Body>,
|
|
@@ -177,35 +197,33 @@ async fn dynamic_handler_inner(
|
|
|
177
197
|
"action" => {
|
|
178
198
|
let action_name = route.value.as_str().unwrap_or("").trim();
|
|
179
199
|
if action_name.is_empty() {
|
|
180
|
-
return (
|
|
181
|
-
StatusCode::INTERNAL_SERVER_ERROR,
|
|
182
|
-
"Invalid action name",
|
|
183
|
-
)
|
|
184
|
-
.into_response();
|
|
200
|
+
return (StatusCode::INTERNAL_SERVER_ERROR, "Invalid action name").into_response();
|
|
185
201
|
}
|
|
186
202
|
|
|
187
|
-
|
|
188
|
-
let
|
|
189
|
-
let
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
+
}
|
|
195
214
|
};
|
|
196
215
|
|
|
197
216
|
let action_path = actions_dir.join(format!("{}.jsbundle", action_name));
|
|
198
217
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
218
|
+
if !action_path.exists() {
|
|
219
|
+
return (
|
|
220
|
+
StatusCode::NOT_FOUND,
|
|
221
|
+
format!("Action bundle not found: {:?}", action_path),
|
|
222
|
+
)
|
|
204
223
|
.into_response();
|
|
205
|
-
|
|
206
|
-
|
|
224
|
+
}
|
|
207
225
|
|
|
208
|
-
let js_code = match fs::read_to_string(action_path) {
|
|
226
|
+
let js_code = match fs::read_to_string(&action_path) {
|
|
209
227
|
Ok(v) => v,
|
|
210
228
|
Err(e) => {
|
|
211
229
|
return (
|
|
@@ -216,14 +234,14 @@ async fn dynamic_handler_inner(
|
|
|
216
234
|
}
|
|
217
235
|
};
|
|
218
236
|
|
|
219
|
-
// Build env
|
|
237
|
+
// Build env object
|
|
220
238
|
let mut env_map = serde_json::Map::new();
|
|
221
239
|
for (k, v) in std::env::vars() {
|
|
222
240
|
env_map.insert(k, Value::String(v));
|
|
223
241
|
}
|
|
224
242
|
let env_json = Value::Object(env_map);
|
|
225
243
|
|
|
226
|
-
// Injected
|
|
244
|
+
// Injected script: sets process.env and __titan_req and invokes action function.
|
|
227
245
|
let injected = format!(
|
|
228
246
|
r#"
|
|
229
247
|
globalThis.process = {{ env: {} }};
|
|
@@ -237,7 +255,6 @@ async fn dynamic_handler_inner(
|
|
|
237
255
|
action_name
|
|
238
256
|
);
|
|
239
257
|
|
|
240
|
-
// Create Boa context, inject t.fetch, evaluate
|
|
241
258
|
let mut ctx = Context::default();
|
|
242
259
|
inject_t_fetch(&mut ctx);
|
|
243
260
|
|
|
@@ -246,7 +263,6 @@ async fn dynamic_handler_inner(
|
|
|
246
263
|
Err(e) => return Json(json_error(e.to_string())).into_response(),
|
|
247
264
|
};
|
|
248
265
|
|
|
249
|
-
// to_json returns Result<Value, JsError> in Boa 0.20
|
|
250
266
|
let result_json: Value = match result.to_json(&mut ctx) {
|
|
251
267
|
Ok(v) => v,
|
|
252
268
|
Err(e) => json_error(e.to_string()),
|
|
@@ -272,25 +288,30 @@ fn json_error(msg: String) -> Value {
|
|
|
272
288
|
serde_json::json!({ "error": msg })
|
|
273
289
|
}
|
|
274
290
|
|
|
275
|
-
// Entrypoint
|
|
291
|
+
// Entrypoint ---------------------------------------------------------------
|
|
276
292
|
|
|
277
293
|
#[tokio::main]
|
|
278
294
|
async fn main() -> Result<()> {
|
|
279
295
|
dotenvy::dotenv().ok();
|
|
280
296
|
|
|
297
|
+
// Load routes.json (expected at runtime root)
|
|
281
298
|
let raw = fs::read_to_string("./routes.json").unwrap_or_else(|_| "{}".to_string());
|
|
282
299
|
let json: Value = serde_json::from_str(&raw).unwrap_or_default();
|
|
283
300
|
|
|
284
301
|
let port = json["__config"]["port"].as_u64().unwrap_or(3000);
|
|
285
|
-
|
|
286
302
|
let routes_json = json["routes"].clone();
|
|
287
|
-
let map: HashMap<String, RouteVal> =
|
|
288
|
-
|
|
303
|
+
let map: HashMap<String, RouteVal> = serde_json::from_value(routes_json).unwrap_or_default();
|
|
304
|
+
|
|
305
|
+
// Project root — heuristics: try current_dir()
|
|
306
|
+
let project_root = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
|
289
307
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
+
);
|
|
294
315
|
|
|
295
316
|
let state = AppState {
|
|
296
317
|
routes: Arc::new(map),
|
|
@@ -304,18 +325,15 @@ async fn main() -> Result<()> {
|
|
|
304
325
|
|
|
305
326
|
let listener = TcpListener::bind(format!("0.0.0.0:{}", port)).await?;
|
|
306
327
|
|
|
307
|
-
//
|
|
308
|
-
// TITAN BANNER
|
|
309
|
-
//
|
|
328
|
+
// Banner (yellow-orange) and server info
|
|
310
329
|
println!("\n\x1b[38;5;208m████████╗██╗████████╗ █████╗ ███╗ ██╗");
|
|
311
330
|
println!("╚══██╔══╝██║╚══██╔══╝██╔══██╗████╗ ██║");
|
|
312
331
|
println!(" ██║ ██║ ██║ ███████║██╔██╗ ██║");
|
|
313
332
|
println!(" ██║ ██║ ██║ ██╔══██║██║╚██╗██║");
|
|
314
333
|
println!(" ██║ ██║ ██║ ██║ ██║██║ ╚████║");
|
|
315
334
|
println!(" ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝\x1b[0m\n");
|
|
316
|
-
|
|
317
335
|
println!("\x1b[38;5;39mTitan server running at:\x1b[0m http://localhost:{}", port);
|
|
318
|
-
|
|
336
|
+
|
|
319
337
|
axum::serve(listener, app).await?;
|
|
320
338
|
Ok(())
|
|
321
339
|
}
|