@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.
Files changed (53) hide show
  1. package/README.md +86 -200
  2. package/index.js +87 -23
  3. package/package.json +4 -3
  4. package/templates/js/_gitignore +37 -0
  5. package/templates/{package.json → js/package.json} +3 -0
  6. package/templates/{server → js/server}/actions/hello.jsbundle +4 -1
  7. package/templates/{server → js/server}/src/extensions.rs +149 -17
  8. package/templates/{server → js/server}/src/main.rs +1 -6
  9. package/templates/{titan → js/titan}/bundle.js +22 -9
  10. package/templates/js/titan/dev.js +258 -0
  11. package/templates/js/titan/titan.js +122 -0
  12. package/templates/rust/Dockerfile +66 -0
  13. package/templates/rust/_dockerignore +3 -0
  14. package/templates/rust/_gitignore +37 -0
  15. package/templates/rust/app/actions/hello.js +5 -0
  16. package/templates/rust/app/actions/rust_hello.rs +14 -0
  17. package/templates/rust/app/app.js +11 -0
  18. package/templates/rust/app/titan.d.ts +101 -0
  19. package/templates/rust/jsconfig.json +19 -0
  20. package/templates/rust/package.json +13 -0
  21. package/templates/rust/server/Cargo.lock +2869 -0
  22. package/templates/rust/server/Cargo.toml +39 -0
  23. package/templates/rust/server/action_map.json +3 -0
  24. package/templates/rust/server/actions/hello.jsbundle +47 -0
  25. package/templates/rust/server/routes.json +22 -0
  26. package/templates/rust/server/src/action_management.rs +131 -0
  27. package/templates/rust/server/src/actions_rust/mod.rs +19 -0
  28. package/templates/rust/server/src/actions_rust/rust_hello.rs +14 -0
  29. package/templates/rust/server/src/errors.rs +10 -0
  30. package/templates/rust/server/src/extensions.rs +989 -0
  31. package/templates/rust/server/src/main.rs +437 -0
  32. package/templates/rust/server/src/utils.rs +33 -0
  33. package/templates/rust/titan/bundle.js +157 -0
  34. package/templates/rust/titan/dev.js +266 -0
  35. package/templates/{titan → rust/titan}/titan.js +122 -98
  36. package/titanpl-sdk/templates/Dockerfile +4 -17
  37. package/titanpl-sdk/templates/server/src/extensions.rs +218 -423
  38. package/titanpl-sdk/templates/server/src/main.rs +68 -134
  39. package/templates/_gitignore +0 -25
  40. package/templates/titan/dev.js +0 -144
  41. /package/templates/{Dockerfile → js/Dockerfile} +0 -0
  42. /package/templates/{.dockerignore → js/_dockerignore} +0 -0
  43. /package/templates/{app → js/app}/actions/hello.js +0 -0
  44. /package/templates/{app → js/app}/app.js +0 -0
  45. /package/templates/{app → js/app}/titan.d.ts +0 -0
  46. /package/templates/{jsconfig.json → js/jsconfig.json} +0 -0
  47. /package/templates/{server → js/server}/Cargo.lock +0 -0
  48. /package/templates/{server → js/server}/Cargo.toml +0 -0
  49. /package/templates/{server → js/server}/action_map.json +0 -0
  50. /package/templates/{server → js/server}/routes.json +0 -0
  51. /package/templates/{server → js/server}/src/action_management.rs +0 -0
  52. /package/templates/{server → js/server}/src/errors.rs +0 -0
  53. /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(t) => t,
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(t) => retval.set(v8_str(scope, &t).into()),
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
- println!("\n\x1b[38;5;208m████████╗██╗████████╗ █████╗ ███╗ ██╗");
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
- console.log("[Titan] Bundling actions...");
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
- for (const file of fs.readdirSync(outDir)) {
16
- fs.unlinkSync(path.join(outDir, file));
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}"] = fn;
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;