@ezetgalaxy/titan 26.7.5 → 26.8.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/README.md +86 -200
- package/index.js +87 -23
- package/package.json +4 -3
- package/templates/js/_gitignore +37 -0
- package/templates/{package.json → js/package.json} +3 -0
- package/templates/{server → js/server}/actions/hello.jsbundle +4 -1
- package/templates/{server → js/server}/src/extensions.rs +149 -17
- package/templates/{server → js/server}/src/main.rs +1 -6
- package/templates/{titan → js/titan}/bundle.js +22 -9
- package/templates/js/titan/dev.js +258 -0
- package/templates/js/titan/titan.js +122 -0
- package/templates/rust/Dockerfile +66 -0
- package/templates/rust/_dockerignore +3 -0
- package/templates/rust/_gitignore +37 -0
- package/templates/rust/app/actions/hello.js +5 -0
- package/templates/rust/app/actions/rust_hello.rs +14 -0
- package/templates/rust/app/app.js +11 -0
- package/templates/rust/app/titan.d.ts +101 -0
- package/templates/rust/jsconfig.json +19 -0
- package/templates/rust/package.json +13 -0
- package/templates/rust/server/Cargo.lock +2869 -0
- package/templates/rust/server/Cargo.toml +39 -0
- package/templates/rust/server/action_map.json +3 -0
- package/templates/rust/server/actions/hello.jsbundle +47 -0
- package/templates/rust/server/routes.json +22 -0
- package/templates/rust/server/src/action_management.rs +131 -0
- package/templates/rust/server/src/actions_rust/mod.rs +19 -0
- package/templates/rust/server/src/actions_rust/rust_hello.rs +14 -0
- package/templates/rust/server/src/errors.rs +10 -0
- package/templates/rust/server/src/extensions.rs +989 -0
- package/templates/rust/server/src/main.rs +437 -0
- package/templates/rust/server/src/utils.rs +33 -0
- package/templates/rust/titan/bundle.js +157 -0
- package/templates/rust/titan/dev.js +266 -0
- package/templates/{titan → rust/titan}/titan.js +122 -98
- package/titanpl-sdk/templates/Dockerfile +4 -17
- package/titanpl-sdk/templates/server/src/extensions.rs +218 -423
- package/titanpl-sdk/templates/server/src/main.rs +68 -134
- package/templates/_gitignore +0 -25
- package/templates/titan/dev.js +0 -144
- /package/templates/{Dockerfile → js/Dockerfile} +0 -0
- /package/templates/{.dockerignore → js/_dockerignore} +0 -0
- /package/templates/{app → js/app}/actions/hello.js +0 -0
- /package/templates/{app → js/app}/app.js +0 -0
- /package/templates/{app → js/app}/titan.d.ts +0 -0
- /package/templates/{jsconfig.json → js/jsconfig.json} +0 -0
- /package/templates/{server → js/server}/Cargo.lock +0 -0
- /package/templates/{server → js/server}/Cargo.toml +0 -0
- /package/templates/{server → js/server}/action_map.json +0 -0
- /package/templates/{server → js/server}/routes.json +0 -0
- /package/templates/{server → js/server}/src/action_management.rs +0 -0
- /package/templates/{server → js/server}/src/errors.rs +0 -0
- /package/templates/{server → js/server}/src/utils.rs +0 -0
|
@@ -3,6 +3,7 @@ use bcrypt::{DEFAULT_COST, hash, verify};
|
|
|
3
3
|
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode};
|
|
4
4
|
use reqwest::{
|
|
5
5
|
blocking::Client,
|
|
6
|
+
Method,
|
|
6
7
|
header::{HeaderMap, HeaderName, HeaderValue},
|
|
7
8
|
};
|
|
8
9
|
use serde_json::Value;
|
|
@@ -18,6 +19,148 @@ use std::fs;
|
|
|
18
19
|
use std::sync::Mutex;
|
|
19
20
|
use walkdir::WalkDir;
|
|
20
21
|
|
|
22
|
+
|
|
23
|
+
// ----------------------------------------------------------------------------
|
|
24
|
+
// RUST ACTION API
|
|
25
|
+
// ----------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
pub struct T {
|
|
28
|
+
pub jwt: Jwt,
|
|
29
|
+
pub password: Password,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#[allow(non_upper_case_globals)]
|
|
33
|
+
pub static t: T = T {
|
|
34
|
+
jwt: Jwt,
|
|
35
|
+
password: Password,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
pub struct Jwt;
|
|
39
|
+
impl Jwt {
|
|
40
|
+
pub fn sign(&self, payload: Value, secret: &str, options: Option<Value>) -> anyhow::Result<String> {
|
|
41
|
+
let mut final_payload = match payload {
|
|
42
|
+
Value::Object(map) => map,
|
|
43
|
+
_ => serde_json::Map::new(), // Should probably error or handle string payload like JS
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
if let Some(opts) = options {
|
|
47
|
+
if let Some(exp_val) = opts.get("expiresIn") {
|
|
48
|
+
// Handle both number (seconds) and string ("1h")
|
|
49
|
+
let seconds = if let Some(n) = exp_val.as_u64() {
|
|
50
|
+
Some(n)
|
|
51
|
+
} else if let Some(s) = exp_val.as_str() {
|
|
52
|
+
parse_expires_in(s)
|
|
53
|
+
} else {
|
|
54
|
+
None
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
if let Some(sec) = seconds {
|
|
58
|
+
let now = SystemTime::now()
|
|
59
|
+
.duration_since(UNIX_EPOCH)
|
|
60
|
+
.unwrap()
|
|
61
|
+
.as_secs();
|
|
62
|
+
final_payload.insert("exp".to_string(), Value::Number(serde_json::Number::from(now + sec)));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let token = encode(
|
|
68
|
+
&Header::default(),
|
|
69
|
+
&Value::Object(final_payload),
|
|
70
|
+
&EncodingKey::from_secret(secret.as_bytes()),
|
|
71
|
+
)?;
|
|
72
|
+
Ok(token)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
pub fn verify(&self, token: &str, secret: &str) -> anyhow::Result<Value> {
|
|
76
|
+
let mut validation = Validation::default();
|
|
77
|
+
validation.validate_exp = true;
|
|
78
|
+
|
|
79
|
+
let data = decode::<Value>(
|
|
80
|
+
token,
|
|
81
|
+
&DecodingKey::from_secret(secret.as_bytes()),
|
|
82
|
+
&validation,
|
|
83
|
+
)?;
|
|
84
|
+
Ok(data.claims)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
pub struct Password;
|
|
89
|
+
impl Password {
|
|
90
|
+
pub fn hash(&self, password: &str) -> anyhow::Result<String> {
|
|
91
|
+
let h = hash(password, DEFAULT_COST)?;
|
|
92
|
+
Ok(h)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
pub fn verify(&self, password: &str, hash_str: &str) -> bool {
|
|
96
|
+
verify(password, hash_str).unwrap_or(false)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
impl T {
|
|
101
|
+
pub fn log(&self, msg: impl std::fmt::Display) {
|
|
102
|
+
println!(
|
|
103
|
+
"{} {}",
|
|
104
|
+
blue("[Titan]"),
|
|
105
|
+
gray(&format!("\x1b[90mlog(rust)\x1b[0m\x1b[97m: {}\x1b[0m", msg))
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
pub fn read(&self, path: &str) -> anyhow::Result<String> {
|
|
110
|
+
let root = std::env::current_dir()?;
|
|
111
|
+
let target = root.join(path);
|
|
112
|
+
let target = target.canonicalize()?;
|
|
113
|
+
Ok(fs::read_to_string(target)?)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
pub async fn fetch(&self, url: &str, options: Option<FetchOptions>) -> anyhow::Result<FetchResponse> {
|
|
117
|
+
let client = reqwest::Client::new();
|
|
118
|
+
let opts = options.unwrap_or_default();
|
|
119
|
+
|
|
120
|
+
let mut req = client.request(opts.method.parse().unwrap_or(Method::GET), url);
|
|
121
|
+
|
|
122
|
+
if let Some(headers) = opts.headers {
|
|
123
|
+
let mut map = HeaderMap::new();
|
|
124
|
+
for (k, v) in headers {
|
|
125
|
+
if let (Ok(name), Ok(val)) = (
|
|
126
|
+
HeaderName::from_bytes(k.as_bytes()),
|
|
127
|
+
HeaderValue::from_str(&v),
|
|
128
|
+
) {
|
|
129
|
+
map.insert(name, val);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
req = req.headers(map);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if let Some(body) = opts.body {
|
|
136
|
+
req = req.body(body);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let res = req.send().await?;
|
|
140
|
+
let status = res.status().as_u16();
|
|
141
|
+
let text = res.text().await?;
|
|
142
|
+
|
|
143
|
+
Ok(FetchResponse {
|
|
144
|
+
status,
|
|
145
|
+
body: text,
|
|
146
|
+
ok: status >= 200 && status < 300
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
#[derive(Default)]
|
|
152
|
+
pub struct FetchOptions {
|
|
153
|
+
pub method: String,
|
|
154
|
+
pub headers: Option<std::collections::HashMap<String, String>>,
|
|
155
|
+
pub body: Option<String>,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
pub struct FetchResponse {
|
|
159
|
+
pub status: u16,
|
|
160
|
+
pub body: String,
|
|
161
|
+
pub ok: bool,
|
|
162
|
+
}
|
|
163
|
+
|
|
21
164
|
// ----------------------------------------------------------------------------
|
|
22
165
|
// GLOBAL REGISTRY
|
|
23
166
|
// ----------------------------------------------------------------------------
|
|
@@ -109,14 +252,16 @@ pub fn load_project_extensions(root: PathBuf) {
|
|
|
109
252
|
search_dirs.sort();
|
|
110
253
|
search_dirs.dedup();
|
|
111
254
|
|
|
112
|
-
println!("{} Scanning extension directories:", blue("[Titan]"));
|
|
255
|
+
// println!("{} Scanning extension directories:", blue("[Titan]"));
|
|
113
256
|
for d in &search_dirs {
|
|
257
|
+
/*
|
|
114
258
|
let label = if d.to_string_lossy().contains(".ext") {
|
|
115
259
|
crate::utils::green("(Production)")
|
|
116
260
|
} else {
|
|
117
261
|
crate::utils::yellow("(Development)")
|
|
118
262
|
};
|
|
119
263
|
println!(" • {} {}", d.display(), label);
|
|
264
|
+
*/
|
|
120
265
|
}
|
|
121
266
|
|
|
122
267
|
// =====================================================
|
|
@@ -245,20 +390,7 @@ pub fn load_project_extensions(root: PathBuf) {
|
|
|
245
390
|
// 5. Store registry globally
|
|
246
391
|
// =====================================================
|
|
247
392
|
if modules.is_empty() {
|
|
248
|
-
println!("{} {}", blue("[Titan]"), crate::utils::yellow("No extensions loaded."));
|
|
249
|
-
// Debug: list files in search dirs to assist debugging
|
|
250
|
-
for dir in &search_dirs {
|
|
251
|
-
if dir.exists() {
|
|
252
|
-
println!("{} Listing contents of {}:", blue("[Titan]"), dir.display());
|
|
253
|
-
for entry in WalkDir::new(dir).max_depth(5) {
|
|
254
|
-
if let Ok(e) = entry {
|
|
255
|
-
println!(" - {}", e.path().display());
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
} else {
|
|
259
|
-
println!("{} Directory not found: {}", blue("[Titan]"), dir.display());
|
|
260
|
-
}
|
|
261
|
-
}
|
|
393
|
+
// println!("{} {}", blue("[Titan]"), crate::utils::yellow("No extensions loaded."));
|
|
262
394
|
}
|
|
263
395
|
|
|
264
396
|
*REGISTRY.lock().unwrap() = Some(Registry {
|
|
@@ -333,7 +465,7 @@ fn native_read(
|
|
|
333
465
|
|
|
334
466
|
// 3. Canonicalize (resolves ../)
|
|
335
467
|
let target = match joined.canonicalize() {
|
|
336
|
-
Ok(
|
|
468
|
+
Ok(target) => target,
|
|
337
469
|
Err(_) => {
|
|
338
470
|
throw(scope, &format!("t.read: file not found: {}", path_str));
|
|
339
471
|
return;
|
|
@@ -554,7 +686,7 @@ fn native_jwt_sign(
|
|
|
554
686
|
);
|
|
555
687
|
|
|
556
688
|
match token {
|
|
557
|
-
Ok(
|
|
689
|
+
Ok(tok) => retval.set(v8_str(scope, &tok).into()),
|
|
558
690
|
Err(e) => throw(scope, &e.to_string()),
|
|
559
691
|
}
|
|
560
692
|
}
|
|
@@ -368,12 +368,7 @@ async fn main() -> Result<()> {
|
|
|
368
368
|
|
|
369
369
|
let listener = TcpListener::bind(format!("0.0.0.0:{}", port)).await?;
|
|
370
370
|
|
|
371
|
-
|
|
372
|
-
println!("╚══██╔══╝██║╚══██╔══╝██╔══██╗████╗ ██║");
|
|
373
|
-
println!(" ██║ ██║ ██║ ███████║██╔██╗ ██║");
|
|
374
|
-
println!(" ██║ ██║ ██║ ██╔══██║██║╚██╗██║");
|
|
375
|
-
println!(" ██║ ██║ ██║ ██║ ██║██║ ╚████║");
|
|
376
|
-
println!(" ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝\x1b[0m\n");
|
|
371
|
+
|
|
377
372
|
println!(
|
|
378
373
|
"\x1b[38;5;39mTitan server running at:\x1b[0m http://localhost:{}",
|
|
379
374
|
port
|
|
@@ -7,16 +7,28 @@ const actionsDir = path.join(root, "app", "actions");
|
|
|
7
7
|
const outDir = path.join(root, "server", "actions");
|
|
8
8
|
|
|
9
9
|
export async function bundle() {
|
|
10
|
-
|
|
10
|
+
const start = Date.now();
|
|
11
|
+
await bundleJs();
|
|
12
|
+
// console.log(`[Titan] Bundle finished in ${((Date.now() - start) / 1000).toFixed(2)}s`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function bundleJs() {
|
|
16
|
+
// console.log("[Titan] Bundling JS actions...");
|
|
11
17
|
|
|
12
18
|
fs.mkdirSync(outDir, { recursive: true });
|
|
13
19
|
|
|
14
20
|
// Clean old bundles
|
|
15
|
-
|
|
16
|
-
fs.
|
|
21
|
+
if (fs.existsSync(outDir)) {
|
|
22
|
+
const oldFiles = fs.readdirSync(outDir);
|
|
23
|
+
for (const file of oldFiles) {
|
|
24
|
+
fs.unlinkSync(path.join(outDir, file));
|
|
25
|
+
}
|
|
17
26
|
}
|
|
18
27
|
|
|
19
28
|
const files = fs.readdirSync(actionsDir).filter(f => f.endsWith(".js") || f.endsWith(".ts"));
|
|
29
|
+
if (files.length === 0) return;
|
|
30
|
+
|
|
31
|
+
// console.log(`[Titan] Bundling ${files.length} JS actions...`);
|
|
20
32
|
|
|
21
33
|
for (const file of files) {
|
|
22
34
|
const actionName = path.basename(file, path.extname(file));
|
|
@@ -26,7 +38,7 @@ export async function bundle() {
|
|
|
26
38
|
// Rust runtime expects `.jsbundle` extension — consistent with previous design
|
|
27
39
|
const outfile = path.join(outDir, actionName + ".jsbundle");
|
|
28
40
|
|
|
29
|
-
console.log(`[Titan] Bundling ${entry} → ${outfile}`);
|
|
41
|
+
// console.log(`[Titan] Bundling ${entry} → ${outfile}`);
|
|
30
42
|
|
|
31
43
|
await esbuild.build({
|
|
32
44
|
entryPoints: [entry],
|
|
@@ -36,6 +48,7 @@ export async function bundle() {
|
|
|
36
48
|
globalName: "__titan_exports",
|
|
37
49
|
platform: "neutral",
|
|
38
50
|
target: "es2020",
|
|
51
|
+
logLevel: "silent",
|
|
39
52
|
banner: {
|
|
40
53
|
js: "const defineAction = (fn) => fn;"
|
|
41
54
|
},
|
|
@@ -51,15 +64,15 @@ export async function bundle() {
|
|
|
51
64
|
throw new Error("[Titan] Action '${actionName}' not found or not a function");
|
|
52
65
|
}
|
|
53
66
|
|
|
54
|
-
globalThis["${actionName}"] =
|
|
67
|
+
globalThis["${actionName}"] = function(request_arg) {
|
|
68
|
+
globalThis.req = request_arg;
|
|
69
|
+
return fn(request_arg);
|
|
70
|
+
};
|
|
55
71
|
})();
|
|
56
72
|
`
|
|
57
73
|
}
|
|
58
74
|
});
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
75
|
}
|
|
63
76
|
|
|
64
|
-
console.log("[Titan] Bundling finished.");
|
|
77
|
+
// console.log("[Titan] JS Bundling finished.");
|
|
65
78
|
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import chokidar from "chokidar";
|
|
2
|
+
import { spawn, execSync } from "child_process";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import { bundle } from "./bundle.js";
|
|
7
|
+
|
|
8
|
+
// Required for __dirname in ES modules
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
// Colors
|
|
14
|
+
import { createRequire } from "module";
|
|
15
|
+
|
|
16
|
+
// Colors
|
|
17
|
+
const cyan = (t) => `\x1b[36m${t}\x1b[0m`;
|
|
18
|
+
const green = (t) => `\x1b[32m${t}\x1b[0m`;
|
|
19
|
+
const yellow = (t) => `\x1b[33m${t}\x1b[0m`;
|
|
20
|
+
const red = (t) => `\x1b[31m${t}\x1b[0m`;
|
|
21
|
+
const gray = (t) => `\x1b[90m${t}\x1b[0m`;
|
|
22
|
+
const bold = (t) => `\x1b[1m${t}\x1b[0m`;
|
|
23
|
+
|
|
24
|
+
function getTitanVersion() {
|
|
25
|
+
try {
|
|
26
|
+
const require = createRequire(import.meta.url);
|
|
27
|
+
const pkgPath = require.resolve("@ezetgalaxy/titan/package.json");
|
|
28
|
+
return JSON.parse(fs.readFileSync(pkgPath, "utf-8")).version;
|
|
29
|
+
} catch (e) {
|
|
30
|
+
try {
|
|
31
|
+
// Check levels up to find the framework root
|
|
32
|
+
let cur = __dirname;
|
|
33
|
+
for (let i = 0; i < 5; i++) {
|
|
34
|
+
const pkgPath = path.join(cur, "package.json");
|
|
35
|
+
if (fs.existsSync(pkgPath)) {
|
|
36
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
37
|
+
if (pkg.name === "@ezetgalaxy/titan") return pkg.version;
|
|
38
|
+
}
|
|
39
|
+
cur = path.join(cur, "..");
|
|
40
|
+
}
|
|
41
|
+
} catch (e2) { }
|
|
42
|
+
}
|
|
43
|
+
return "0.1.0";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let serverProcess = null;
|
|
47
|
+
let isKilling = false;
|
|
48
|
+
|
|
49
|
+
// ... (killServer same as before)
|
|
50
|
+
async function killServer() {
|
|
51
|
+
if (!serverProcess) return;
|
|
52
|
+
|
|
53
|
+
isKilling = true;
|
|
54
|
+
const pid = serverProcess.pid;
|
|
55
|
+
const killPromise = new Promise((resolve) => {
|
|
56
|
+
if (serverProcess.exitCode !== null) return resolve();
|
|
57
|
+
serverProcess.once("close", resolve);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (process.platform === "win32") {
|
|
61
|
+
try {
|
|
62
|
+
execSync(`taskkill /pid ${pid} /f /t`, { stdio: 'ignore' });
|
|
63
|
+
} catch (e) {
|
|
64
|
+
// Ignore errors if process is already dead
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
serverProcess.kill();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
await killPromise;
|
|
72
|
+
} catch (e) { }
|
|
73
|
+
serverProcess = null;
|
|
74
|
+
isKilling = false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const delay = (ms) => new Promise(res => setTimeout(res, ms));
|
|
78
|
+
|
|
79
|
+
let spinnerTimer = null;
|
|
80
|
+
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
81
|
+
let frameIdx = 0;
|
|
82
|
+
|
|
83
|
+
function startSpinner(text) {
|
|
84
|
+
if (spinnerTimer) clearInterval(spinnerTimer);
|
|
85
|
+
process.stdout.write("\x1B[?25l"); // Hide cursor
|
|
86
|
+
spinnerTimer = setInterval(() => {
|
|
87
|
+
process.stdout.write(`\r ${cyan(frames[frameIdx])} ${gray(text)}`);
|
|
88
|
+
frameIdx = (frameIdx + 1) % frames.length;
|
|
89
|
+
}, 80);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function stopSpinner(success = true, text = "") {
|
|
93
|
+
if (spinnerTimer) {
|
|
94
|
+
clearInterval(spinnerTimer);
|
|
95
|
+
spinnerTimer = null;
|
|
96
|
+
}
|
|
97
|
+
process.stdout.write("\r\x1B[K"); // Clear line
|
|
98
|
+
process.stdout.write("\x1B[?25h"); // Show cursor
|
|
99
|
+
if (text) {
|
|
100
|
+
if (success) {
|
|
101
|
+
console.log(` ${green("✔")} ${green(text)}`);
|
|
102
|
+
} else {
|
|
103
|
+
console.log(` ${red("✖")} ${red(text)}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function startRustServer(retryCount = 0) {
|
|
109
|
+
const waitTime = retryCount > 0 ? 500 : 200;
|
|
110
|
+
|
|
111
|
+
await killServer();
|
|
112
|
+
await delay(waitTime);
|
|
113
|
+
|
|
114
|
+
const serverPath = path.join(process.cwd(), "server");
|
|
115
|
+
const startTime = Date.now();
|
|
116
|
+
|
|
117
|
+
startSpinner("Stabilizing your app on its orbit...");
|
|
118
|
+
|
|
119
|
+
let isReady = false;
|
|
120
|
+
let stdoutBuffer = "";
|
|
121
|
+
let buildLogs = "";
|
|
122
|
+
|
|
123
|
+
// If it takes more than 15s, update the message
|
|
124
|
+
const slowTimer = setTimeout(() => {
|
|
125
|
+
if (!isReady && !isKilling) {
|
|
126
|
+
startSpinner("Still stabilizing... (the first orbit takes longer)");
|
|
127
|
+
}
|
|
128
|
+
}, 15000);
|
|
129
|
+
|
|
130
|
+
serverProcess = spawn("cargo", ["run", "--quiet"], {
|
|
131
|
+
cwd: serverPath,
|
|
132
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
133
|
+
env: { ...process.env, CARGO_INCREMENTAL: "1" }
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
serverProcess.on("error", (err) => {
|
|
137
|
+
stopSpinner(false, "Failed to start orbit");
|
|
138
|
+
console.error(red(`[Titan] Error: ${err.message}`));
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
serverProcess.stderr.on("data", (data) => {
|
|
142
|
+
const str = data.toString();
|
|
143
|
+
if (isReady) {
|
|
144
|
+
process.stderr.write(data);
|
|
145
|
+
} else {
|
|
146
|
+
buildLogs += str;
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
serverProcess.stdout.on("data", (data) => {
|
|
151
|
+
const out = data.toString();
|
|
152
|
+
|
|
153
|
+
if (!isReady) {
|
|
154
|
+
stdoutBuffer += out;
|
|
155
|
+
if (stdoutBuffer.includes("Titan server running") || stdoutBuffer.includes("████████╗")) {
|
|
156
|
+
isReady = true;
|
|
157
|
+
clearTimeout(slowTimer);
|
|
158
|
+
stopSpinner(true, "Your app is now orbiting Titan Planet");
|
|
159
|
+
process.stdout.write(stdoutBuffer);
|
|
160
|
+
stdoutBuffer = "";
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
process.stdout.write(data);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
serverProcess.on("close", async (code) => {
|
|
168
|
+
clearTimeout(slowTimer);
|
|
169
|
+
if (isKilling) return;
|
|
170
|
+
const runTime = Date.now() - startTime;
|
|
171
|
+
|
|
172
|
+
if (code !== 0 && code !== null) {
|
|
173
|
+
stopSpinner(false, "Orbit stabilization failed");
|
|
174
|
+
if (!isReady) {
|
|
175
|
+
console.log(gray("\n--- Build Logs ---"));
|
|
176
|
+
console.log(buildLogs);
|
|
177
|
+
console.log(gray("------------------\n"));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (runTime < 15000 && retryCount < 5) {
|
|
181
|
+
await delay(2000);
|
|
182
|
+
await startRustServer(retryCount + 1);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function rebuild() {
|
|
189
|
+
try {
|
|
190
|
+
execSync("node app/app.js", { stdio: "ignore" });
|
|
191
|
+
// bundle is called inside app.js (t.start)
|
|
192
|
+
} catch (e) {
|
|
193
|
+
stopSpinner(false, "Failed to prepare runtime");
|
|
194
|
+
console.log(red(`[Titan] Error: ${e.message}`));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function startDev() {
|
|
199
|
+
const root = process.cwd();
|
|
200
|
+
const actionsDir = path.join(root, "app", "actions");
|
|
201
|
+
let hasRust = false;
|
|
202
|
+
if (fs.existsSync(actionsDir)) {
|
|
203
|
+
hasRust = fs.readdirSync(actionsDir).some(f => f.endsWith(".rs"));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const mode = hasRust ? "Rust + JS Actions" : "JS Actions";
|
|
207
|
+
const version = getTitanVersion();
|
|
208
|
+
|
|
209
|
+
console.clear();
|
|
210
|
+
console.log("");
|
|
211
|
+
console.log(` ${bold(cyan("Titan Planet"))} ${gray("v" + version)} ${yellow("[ Dev Mode ]")}`);
|
|
212
|
+
console.log("");
|
|
213
|
+
console.log(` ${gray("Type: ")} ${mode}`);
|
|
214
|
+
console.log(` ${gray("Hot Reload: ")} ${green("Enabled")}`);
|
|
215
|
+
|
|
216
|
+
if (fs.existsSync(path.join(root, ".env"))) {
|
|
217
|
+
console.log(` ${gray("Env: ")} ${yellow("Loaded")}`);
|
|
218
|
+
}
|
|
219
|
+
console.log("");
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
await rebuild();
|
|
223
|
+
await startRustServer();
|
|
224
|
+
} catch (e) {
|
|
225
|
+
// console.log(red("[Titan] Initial build failed. Waiting for changes..."));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const watcher = chokidar.watch(["app", ".env"], {
|
|
229
|
+
ignoreInitial: true,
|
|
230
|
+
awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 }
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
let timer = null;
|
|
234
|
+
watcher.on("all", async (event, file) => {
|
|
235
|
+
if (timer) clearTimeout(timer);
|
|
236
|
+
timer = setTimeout(async () => {
|
|
237
|
+
try {
|
|
238
|
+
await killServer();
|
|
239
|
+
await rebuild();
|
|
240
|
+
await startRustServer();
|
|
241
|
+
} catch (e) {
|
|
242
|
+
// console.log(red("[Titan] Build failed -- waiting for changes..."));
|
|
243
|
+
}
|
|
244
|
+
}, 300);
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function handleExit() {
|
|
249
|
+
stopSpinner();
|
|
250
|
+
console.log(gray("\n[Titan] Stopping server..."));
|
|
251
|
+
await killServer();
|
|
252
|
+
process.exit(0);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
process.on("SIGINT", handleExit);
|
|
256
|
+
process.on("SIGTERM", handleExit);
|
|
257
|
+
|
|
258
|
+
startDev();
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { bundle } from "./bundle.js";
|
|
4
|
+
|
|
5
|
+
const cyan = (t) => `\x1b[36m${t}\x1b[0m`;
|
|
6
|
+
const green = (t) => `\x1b[32m${t}\x1b[0m`;
|
|
7
|
+
|
|
8
|
+
const routes = {};
|
|
9
|
+
const dynamicRoutes = {};
|
|
10
|
+
const actionMap = {};
|
|
11
|
+
|
|
12
|
+
function addRoute(method, route) {
|
|
13
|
+
const key = `${method.toUpperCase()}:${route}`;
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
reply(value) {
|
|
18
|
+
routes[key] = {
|
|
19
|
+
type: typeof value === "object" ? "json" : "text",
|
|
20
|
+
value
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
action(name) {
|
|
25
|
+
if (route.includes(":")) {
|
|
26
|
+
if (!dynamicRoutes[method]) dynamicRoutes[method] = [];
|
|
27
|
+
dynamicRoutes[method].push({
|
|
28
|
+
method: method.toUpperCase(),
|
|
29
|
+
pattern: route,
|
|
30
|
+
action: name
|
|
31
|
+
});
|
|
32
|
+
} else {
|
|
33
|
+
routes[key] = {
|
|
34
|
+
type: "action",
|
|
35
|
+
value: name
|
|
36
|
+
};
|
|
37
|
+
actionMap[key] = name;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @typedef {Object} RouteHandler
|
|
45
|
+
* @property {(value: any) => void} reply - Send a direct response
|
|
46
|
+
* @property {(name: string) => void} action - Bind to a server-side action
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Titan App Builder
|
|
51
|
+
*/
|
|
52
|
+
const t = {
|
|
53
|
+
/**
|
|
54
|
+
* Define a GET route
|
|
55
|
+
* @param {string} route
|
|
56
|
+
* @returns {RouteHandler}
|
|
57
|
+
*/
|
|
58
|
+
get(route) {
|
|
59
|
+
return addRoute("GET", route);
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Define a POST route
|
|
64
|
+
* @param {string} route
|
|
65
|
+
* @returns {RouteHandler}
|
|
66
|
+
*/
|
|
67
|
+
post(route) {
|
|
68
|
+
return addRoute("POST", route);
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
log(module, msg) {
|
|
72
|
+
console.log(`[\x1b[35m${module}\x1b[0m] ${msg}`);
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Start the Titan Server
|
|
77
|
+
* @param {number} [port=3000]
|
|
78
|
+
* @param {string} [msg=""]
|
|
79
|
+
*/
|
|
80
|
+
async start(port = 3000, msg = "") {
|
|
81
|
+
try {
|
|
82
|
+
console.log(cyan("[Titan] Preparing runtime..."));
|
|
83
|
+
await bundle();
|
|
84
|
+
|
|
85
|
+
const base = path.join(process.cwd(), "server");
|
|
86
|
+
if (!fs.existsSync(base)) {
|
|
87
|
+
fs.mkdirSync(base, { recursive: true });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const routesPath = path.join(base, "routes.json");
|
|
91
|
+
const actionMapPath = path.join(base, "action_map.json");
|
|
92
|
+
|
|
93
|
+
fs.writeFileSync(
|
|
94
|
+
routesPath,
|
|
95
|
+
JSON.stringify(
|
|
96
|
+
{
|
|
97
|
+
__config: { port },
|
|
98
|
+
routes,
|
|
99
|
+
__dynamic_routes: Object.values(dynamicRoutes).flat()
|
|
100
|
+
},
|
|
101
|
+
null,
|
|
102
|
+
2
|
|
103
|
+
)
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
fs.writeFileSync(
|
|
107
|
+
actionMapPath,
|
|
108
|
+
JSON.stringify(actionMap, null, 2)
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
console.log(green("✔ Titan metadata written successfully"));
|
|
112
|
+
if (msg) console.log(cyan(msg));
|
|
113
|
+
|
|
114
|
+
} catch (e) {
|
|
115
|
+
console.error(`\x1b[31m[Titan] Build Error: ${e.message}\x1b[0m`);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
export default t;
|