@ezetgalaxy/titan 26.4.1 → 26.6.0

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 CHANGED
@@ -138,7 +138,7 @@ Titan now includes a **complete runtime engine** with the following built-in cap
138
138
 
139
139
  ### 🧠 Action Runtime
140
140
 
141
- * JavaScript actions executed inside a Rust runtime (Boa)
141
+ * JavaScript actions executed inside a Rust runtime (v8)
142
142
  * Automatic action discovery and execution
143
143
  * No `globalThis` required anymore
144
144
  * Safe handling of `undefined` returns
@@ -162,7 +162,26 @@ Each action receives a normalized request object:
162
162
  "path": "/user/90",
163
163
  "params": { "id": "90" },
164
164
  "query": {},
165
- "body": null
165
+ "body": null,
166
+
167
+ "headers": {
168
+ "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
169
+ "accept-encoding": "gzip, deflate, br, zstd",
170
+ "accept-language": "en-US,en;q=0.9",
171
+ "cache-control": "max-age=0",
172
+ "connection": "keep-alive",
173
+ "cookie": "",
174
+ "host": "localhost:3000",
175
+ "sec-ch-ua": "\"Google Chrome\";v=\"143\", \"Chromium\";v=\"143\", \"Not A(Brand\";v=\"24\"",
176
+ "sec-ch-ua-mobile": "?0",
177
+ "sec-ch-ua-platform": "\"Windows\"",
178
+ "sec-fetch-dest": "document",
179
+ "sec-fetch-mode": "navigate",
180
+ "sec-fetch-site": "none",
181
+ "sec-fetch-user": "?1",
182
+ "upgrade-insecure-requests": "1",
183
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"
184
+ },
166
185
  }
167
186
  ```
168
187
 
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  import fs from "fs";
3
3
  import path from "path";
4
4
  import { execSync, spawn } from "child_process";
@@ -78,15 +78,20 @@ const TITAN_VERSION = pkg.version;
78
78
  /* -------------------------------------------------------
79
79
  * Utils
80
80
  * ----------------------------------------------------- */
81
- function copyDir(src, dest) {
81
+ function copyDir(src, dest, excludes = []) {
82
82
  fs.mkdirSync(dest, { recursive: true });
83
83
 
84
84
  for (const file of fs.readdirSync(src)) {
85
+ // Skip excluded files/folders
86
+ if (excludes.includes(file)) {
87
+ continue;
88
+ }
89
+
85
90
  const srcPath = path.join(src, file);
86
91
  const destPath = path.join(dest, file);
87
92
 
88
93
  if (fs.lstatSync(srcPath).isDirectory()) {
89
- copyDir(srcPath, destPath);
94
+ copyDir(srcPath, destPath, excludes);
90
95
  } else {
91
96
  fs.copyFileSync(srcPath, destPath);
92
97
  }
@@ -101,6 +106,7 @@ function help() {
101
106
  ${bold(cyan("Titan Planet"))} v${TITAN_VERSION}
102
107
 
103
108
  ${green("titan init <project>")} Create new Titan project
109
+ ${green("titan create ext <name>")} Create new Titan extension
104
110
  ${green("titan dev")} Dev mode (hot reload)
105
111
  ${green("titan build")} Build production Rust server
106
112
  ${green("titan start")} Start production binary
@@ -131,9 +137,9 @@ function initProject(name) {
131
137
  console.log(cyan(`Creating Titan project → ${target}`));
132
138
 
133
139
  // ----------------------------------------------------------
134
- // 1. Copy full template directory
140
+ // 1. Copy full template directory (excluding extension folder)
135
141
  // ----------------------------------------------------------
136
- copyDir(templateDir, target);
142
+ copyDir(templateDir, target, ["extension"]);
137
143
 
138
144
  // ----------------------------------------------------------
139
145
  // 2. Explicitly install dotfiles
@@ -366,20 +372,223 @@ function updateTitan() {
366
372
 
367
373
 
368
374
 
375
+ /* -------------------------------------------------------
376
+ * CREATE EXTENSION
377
+ * ----------------------------------------------------- */
378
+ function createExtension(name) {
379
+ if (!name) {
380
+ console.log(red("Usage: titan create ext <name>"));
381
+ return;
382
+ }
383
+
384
+
385
+ const folderName = name;
386
+
387
+ const target = path.join(process.cwd(), folderName);
388
+ const templateDir = path.join(__dirname, "templates", "extension");
389
+
390
+ if (fs.existsSync(target)) {
391
+ console.log(yellow(`Folder already exists: ${target}`));
392
+ return;
393
+ }
394
+
395
+ if (!fs.existsSync(templateDir)) {
396
+ console.log(red(`Extension template not found at ${templateDir}`));
397
+ return;
398
+ }
399
+
400
+ console.log(cyan(`Creating Titan extension → ${target}`));
401
+
402
+ // 1. Copy template
403
+ copyDir(templateDir, target);
404
+
405
+ // 2. Process templates (replace {{name}})
406
+ const title = name;
407
+
408
+ // 2a. titan.json
409
+ const titJsonPath = path.join(target, "titan.json");
410
+ if (fs.existsSync(titJsonPath)) {
411
+ let content = fs.readFileSync(titJsonPath, "utf8");
412
+ content = content.replace(/{{name}}/g, title);
413
+ fs.writeFileSync(titJsonPath, content);
414
+ }
415
+
416
+ // 2b. index.js
417
+ const idxPath = path.join(target, "index.js");
418
+ if (fs.existsSync(idxPath)) {
419
+ let content = fs.readFileSync(idxPath, "utf8");
420
+ content = content.replace(/{{name}}/g, title);
421
+ fs.writeFileSync(idxPath, content);
422
+ }
423
+
424
+ // 2c. README.md
425
+ const readmePath = path.join(target, "README.md");
426
+ if (fs.existsSync(readmePath)) {
427
+ let content = fs.readFileSync(readmePath, "utf8");
428
+ content = content.replace(/{{name}}/g, title);
429
+ fs.writeFileSync(readmePath, content);
430
+ }
431
+
432
+ // 2d. package.json
433
+ const pkgPath = path.join(target, "package.json");
434
+ if (fs.existsSync(pkgPath)) {
435
+ let content = fs.readFileSync(pkgPath, "utf8");
436
+ content = content.replace(/{{name}}/g, title);
437
+ fs.writeFileSync(pkgPath, content);
438
+ }
439
+
440
+ // 2e. native/Cargo.toml
441
+ const cargoPath = path.join(target, "native", "Cargo.toml");
442
+ if (fs.existsSync(cargoPath)) {
443
+ let content = fs.readFileSync(cargoPath, "utf8");
444
+ content = content.replace(/{{name}}/g, title);
445
+ fs.writeFileSync(cargoPath, content);
446
+ }
447
+
448
+ console.log(cyan("Installing dependencies..."));
449
+ try {
450
+ execSync("npm install", { cwd: target, stdio: "inherit" });
451
+ } catch (e) {
452
+ console.log(yellow("Warning: Failed to install dependencies. You may need to run `npm install` manually."));
453
+ }
454
+
455
+ console.log(green("✔ Extension created!"));
456
+ console.log(`
457
+ Next steps:
458
+ cd ${name}
459
+ # If you have native code:
460
+ cd native && cargo build --release
461
+ # To test your extension
462
+ titan run ext
463
+ `);
464
+ }
465
+
466
+ function runExtension() {
467
+ const cwd = process.cwd();
468
+ const manifestPath = path.join(cwd, "titan.json");
469
+
470
+ if (!fs.existsSync(manifestPath)) {
471
+ console.log(red("Error: titan.json not found. Are you in an extension folder?"));
472
+ return;
473
+ }
474
+
475
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
476
+ const name = manifest.name;
477
+ console.log(cyan(`Preparing to run extension: ${name}`));
478
+
479
+ // 1. Build Native if exists
480
+ const nativeDir = path.join(cwd, "native");
481
+ if (fs.existsSync(nativeDir)) {
482
+ console.log(cyan("Building native module..."));
483
+ try {
484
+ execSync("cargo build --release", { cwd: nativeDir, stdio: "inherit" });
485
+ } catch (e) {
486
+ console.log(red("Native build failed."));
487
+ return;
488
+ }
489
+ }
490
+
491
+ // 2. Setup Temporary Runner
492
+ const runnerDir = path.join(cwd, ".titan_runner");
493
+ if (fs.existsSync(runnerDir)) {
494
+ fs.rmSync(runnerDir, { recursive: true, force: true });
495
+ }
496
+
497
+ // We need to create a project environment.
498
+ // We can use the templates stored in __dirname
499
+ const templateDir = path.join(__dirname, "templates");
500
+
501
+ console.log(cyan("Setting up test harness..."));
502
+ fs.mkdirSync(runnerDir);
503
+
504
+ // Copy templates/app -> runner/app
505
+ const runnerApp = path.join(runnerDir, "app");
506
+ copyDir(path.join(templateDir, "app"), runnerApp);
507
+
508
+ // Copy templates/server -> runner/server
509
+ const runnerServer = path.join(runnerDir, "server");
510
+ copyDir(path.join(templateDir, "server"), runnerServer);
511
+
512
+ // Create a dummy app.js that uses the extension
513
+ const appJsContent = `
514
+ const extensionName = "${name}";
515
+ t.log("TestRunner", "Loading extension: " + extensionName);
516
+
517
+ // Access the extension
518
+ if (t[extensionName]) {
519
+ t.log("TestRunner", "Extension found on 't'!");
520
+ if (t[extensionName].hello) {
521
+ t[extensionName].hello("Titan User");
522
+ }
523
+ if (t[extensionName].calc) {
524
+ const res = t[extensionName].calc(10, 50);
525
+ t.log("TestRunner", "Calc Result (10+50): " + res);
526
+ }
527
+ } else {
528
+ t.log("TestRunner", "ERROR: Extension not found on 't'");
529
+ }
530
+ `;
531
+ fs.writeFileSync(path.join(runnerApp, "app.js"), appJsContent);
532
+
533
+ // 3. Link Extension
534
+ // We need to simulate 'node_modules/extension_name'
535
+ const runnerNodeModules = path.join(runnerDir, "node_modules");
536
+ fs.mkdirSync(runnerNodeModules, { recursive: true });
537
+
538
+ const extLinkPath = path.join(runnerNodeModules, name);
539
+ // On Windows, symlinks require special permissions, usually.
540
+ // Junctions are safer for directories.
541
+ try {
542
+ fs.symlinkSync(cwd, extLinkPath, "junction");
543
+ } catch (e) {
544
+ // Fallback to copy if symlink fails
545
+ console.log(yellow("Symlink failed, copying extension..."));
546
+ copyDir(cwd, extLinkPath);
547
+ }
548
+
549
+ console.log(cyan("Building test harness server... (this may take a minute)"));
550
+ try {
551
+ execSync("cargo build --release", { cwd: runnerServer, stdio: "inherit" });
552
+ } catch (e) {
553
+ console.log(red("Failed to build test server."));
554
+ return;
555
+ }
556
+
557
+ // 5. Run it
558
+ const isWin = process.platform === "win32";
559
+ const bin = isWin ? "titan-server.exe" : "titan-server";
560
+ const exe = path.join(runnerServer, "target", "release", bin);
561
+
562
+ console.log(bold(green("\n>>> STARTING EXTENSION TEST >>>\n")));
563
+ try {
564
+ // Run inside the runner directory so it finds app/, server/, etc.
565
+ execSync(`"${exe}"`, { cwd: runnerDir, stdio: "inherit" });
566
+ } catch (e) {
567
+ console.log(red("\nTest ended with error or was stopped."));
568
+ }
569
+ }
570
+
369
571
  /* -------------------------------------------------------
370
572
  * ROUTER
371
573
  * ----------------------------------------------------- */
372
- switch (cmd) {
373
- case "init": initProject(args[1]); break;
374
- case "dev": devServer(); break;
375
- case "build": buildProd(); break;
376
- case "start": startProd(); break;
377
- case "update": updateTitan(); break;
378
- case "--version":
379
- case "-v":
380
- case "version":
381
- console.log(cyan(`Titan v${TITAN_VERSION}`));
382
- break;
383
- default:
384
- help();
574
+ // "titan create ext <name>" -> args = ["create", "ext", "calc_ext"]
575
+ if (cmd === "create" && args[1] === "ext") {
576
+ createExtension(args[2]);
577
+ } else if (cmd === "run" && args[1] === "ext") {
578
+ runExtension();
579
+ } else {
580
+ switch (cmd) {
581
+ case "init": initProject(args[1]); break;
582
+ case "dev": devServer(); break;
583
+ case "build": buildProd(); break;
584
+ case "start": startProd(); break;
585
+ case "update": updateTitan(); break;
586
+ case "--version":
587
+ case "-v":
588
+ case "version":
589
+ console.log(cyan(`Titan v${TITAN_VERSION}`));
590
+ break;
591
+ default:
592
+ help();
593
+ }
385
594
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ezetgalaxy/titan",
3
- "version": "26.4.1",
3
+ "version": "26.6.0",
4
4
  "description": "Titan Planet is a JavaScript-first backend framework that embeds JS actions into a Rust + Axum server and ships as a single native binary. Routes are compiled to static metadata; only actions run in the embedded JS runtime. No Node.js. No event loop in production.",
5
5
  "license": "ISC",
6
6
  "author": "ezetgalaxy",
@@ -0,0 +1,104 @@
1
+ # 🪐 Titan Extension: {{name}}
2
+
3
+ > Elevate Titan Planet with custom JavaScript and high-performance Native Rust logic.
4
+
5
+ Welcome to your new Titan extension! This template provides everything you need to build, test, and deploy powerful additions to the Titan project.
6
+
7
+ ---
8
+
9
+ ## 🛠 Project Structure
10
+
11
+ - `index.js`: The JavaScript entry point where you define your extension's API on the global `t` object.
12
+ - `titan.json`: Manifest file defining extension metadata and Native module mappings.
13
+ - `native/`: Directory for Rust source code.
14
+ - `src/lib.rs`: Your Native function implementations.
15
+ - `Cargo.toml`: Rust package and dependency configuration.
16
+ - `jsconfig.json`: Enables full IntelliSense for the Titan Runtime API.
17
+
18
+ ---
19
+
20
+ ## 🚀 Quick Start
21
+
22
+ ### 1. Install Dependencies
23
+ Get full type support in your IDE:
24
+ ```bash
25
+ npm install
26
+ ```
27
+
28
+ ### 2. Build Native Module (Optional)
29
+ If your extension uses Rust, compile it to a dynamic library:
30
+ ```bash
31
+ cd native
32
+ cargo build --release
33
+ cd ..
34
+ ```
35
+
36
+ ### 3. Test the Extension
37
+ Use the Titan SDK to run a local test harness:
38
+ ```bash
39
+ titan run ext
40
+ ```
41
+ *Tip: Visit `http://localhost:3000/test` after starting the runner to see your extension in action!*
42
+
43
+ ---
44
+
45
+ ## 💻 Development Guide
46
+
47
+ ### Writing JavaScript
48
+ Extensions interact with the global `t` object. It's best practice to namespace your extension:
49
+
50
+ ```javascript
51
+ t.{{name}} = {
52
+ myMethod: (val) => {
53
+ t.log("{{name}}", "Doing something...");
54
+ return val * 2;
55
+ }
56
+ };
57
+ ```
58
+
59
+ ### Writing Native Rust Functions
60
+ Native functions should be marked with `#[unsafe(no_mangle)]` and use `extern "C"`:
61
+
62
+ ```rust
63
+ #[unsafe(no_mangle)]
64
+ pub extern "C" fn multiply(a: f64, b: f64) -> f64 {
65
+ a * b
66
+ }
67
+ ```
68
+
69
+ ### Mapping Native Functions in `titan.json`
70
+ Expose your Rust functions to JavaScript by adding them to the `native.functions` section:
71
+
72
+ ```json
73
+ "functions": {
74
+ "add": {
75
+ "symbol": "add",
76
+ "parameters": ["f64", "f64"],
77
+ "result": "f64"
78
+ }
79
+ }
80
+ ```
81
+
82
+ ---
83
+
84
+ ## 🧪 Testing with Titan SDK
85
+
86
+ The `titan run ext` command automates the testing workflow:
87
+ 1. It builds your native code.
88
+ 2. It sets up a temporary Titan project environment.
89
+ 3. It links your extension into `node_modules`.
90
+ 4. It starts the Titan Runtime at `http://localhost:3000`.
91
+
92
+ You can modify the test harness or add custom test cases by exploring the generated `.titan_test_run` directory (it is git-ignored).
93
+
94
+ ---
95
+
96
+ ## 📦 Deployment
97
+ To use your extension in a Titan project:
98
+ 1. Publish your extension to npm or link it locally.
99
+ 2. In your Titan project: `npm install my-extension`.
100
+ 3. The Titan Runtime will automatically detect and load your extension if it contains a `titan.json`.
101
+
102
+ ---
103
+
104
+ Happy coding on Titan Planet! 🚀
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Titan Extension Entry Point
3
+ * You can attach methods to the global `t` object here.
4
+ */
5
+
6
+ // Define your extension Key
7
+ const EXT_KEY = "{{name}}";
8
+
9
+ t.log(EXT_KEY, "Extension loading...");
10
+
11
+ t[EXT_KEY] = {
12
+ // Example pure JavaScript function
13
+ hello: function (name) {
14
+ t.log(EXT_KEY, `Hello ${name} from ${EXT_KEY}!`);
15
+ return `Hello ${name}!`;
16
+ },
17
+
18
+ // Example Wrapper for Native function
19
+ calc: function (a, b) {
20
+ t.log(EXT_KEY, `Calculating ${a} + ${b} natively...`);
21
+ // Assumes the native function 'add' is mapped in titan.json
22
+ return t[EXT_KEY].add(a, b);
23
+ }
24
+ };
25
+
26
+ t.log(EXT_KEY, "Extension loaded!");
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "commonjs",
4
+ "target": "es2021",
5
+ "checkJs": false,
6
+ "allowJs": true,
7
+ "moduleResolution": "node"
8
+ },
9
+ "include": [
10
+ "index.js",
11
+ "node_modules/titan-sdk/index.d.ts"
12
+ ]
13
+ }
@@ -0,0 +1,9 @@
1
+ [package]
2
+ name = "{{name}}_native"
3
+ version = "0.1.0"
4
+ edition = "2024"
5
+
6
+ [lib]
7
+ crate-type = ["cdylib"]
8
+
9
+ [dependencies]
@@ -0,0 +1,5 @@
1
+
2
+ #[unsafe(no_mangle)]
3
+ pub extern "C" fn add(a: f64, b: f64) -> f64 {
4
+ a + b
5
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "{{name}}",
3
+ "version": "1.0.0",
4
+ "description": "A Titan Planet extension",
5
+ "main": "index.js",
6
+ "type": "commonjs",
7
+ "scripts": {
8
+ "test": "echo \"Error: no test specified\" && exit 1"
9
+ },
10
+ "keywords": [
11
+ "titan",
12
+ "extension"
13
+ ],
14
+ "author": "",
15
+ "license": "ISC",
16
+ "dependencies": {
17
+ "titan-sdk": "latest"
18
+ }
19
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "{{name}}",
3
+ "main": "index.js",
4
+ "description": "A Titan extension",
5
+ "native": {
6
+ "path": "native/target/release/{{name}}_native.dll",
7
+ "functions": {
8
+ "add": {
9
+ "symbol": "add",
10
+ "parameters": [
11
+ "f64",
12
+ "f64"
13
+ ],
14
+ "result": "f64"
15
+ }
16
+ }
17
+ }
18
+ }
@@ -23,3 +23,5 @@ regex = "1.10"
23
23
  bcrypt = "0.15"
24
24
  jsonwebtoken = "9"
25
25
  postgres = { version = "0.19", features = ["with-serde_json-1"] }
26
+ libloading = "0.8"
27
+ walkdir = "2"
@@ -1,6 +1,6 @@
1
1
  use v8::JsError;
2
2
 
3
- // A helper to Format Boa Errors
3
+ // A helper to Format v8 Errors
4
4
  pub fn format_js_error(err: JsError, action: &str) -> String {
5
5
  format!(
6
6
  "Action: {}\n{}",
@@ -1,3 +1,4 @@
1
+ #![allow(unused)]
1
2
  use v8;
2
3
  use reqwest::{
3
4
  blocking::Client,
@@ -10,7 +11,142 @@ use serde_json::Value;
10
11
  use jsonwebtoken::{encode, decode, Header, EncodingKey, DecodingKey, Validation};
11
12
  use bcrypt::{hash, verify, DEFAULT_COST};
12
13
 
13
- use crate::utils::{blue, gray, parse_expires_in};
14
+ use crate::utils::{blue, gray, green, parse_expires_in};
15
+ use libloading::{Library};
16
+ use walkdir::WalkDir;
17
+ use std::sync::Mutex;
18
+ use std::collections::HashMap;
19
+ use std::fs;
20
+
21
+ // ----------------------------------------------------------------------------
22
+ // GLOBAL REGISTRY
23
+ // ----------------------------------------------------------------------------
24
+
25
+ static REGISTRY: Mutex<Option<Registry>> = Mutex::new(None);
26
+ #[allow(dead_code)]
27
+ struct Registry {
28
+ _libs: Vec<Library>,
29
+ modules: Vec<ModuleDef>,
30
+ natives: Vec<NativeFnEntry>, // Flattened list of all native functions
31
+ }
32
+
33
+ #[derive(Clone)]
34
+ struct ModuleDef {
35
+ name: String,
36
+ js: String,
37
+ native_indices: HashMap<String, usize>, // Function Name -> Index in REGISTRY.natives
38
+ }
39
+
40
+ struct NativeFnEntry {
41
+ ptr: usize,
42
+ sig: Signature,
43
+ }
44
+
45
+ #[derive(Clone, Copy)]
46
+ enum Signature {
47
+ F64TwoArgsRetF64,
48
+ Unknown,
49
+ }
50
+
51
+ #[derive(serde::Deserialize)]
52
+ struct TitanConfig {
53
+ name: String,
54
+ main: String,
55
+ native: Option<TitanNativeConfig>,
56
+ }
57
+ #[derive(serde::Deserialize)]
58
+ struct TitanNativeConfig {
59
+ path: String,
60
+ functions: HashMap<String, TitanNativeFunc>,
61
+ }
62
+ #[derive(serde::Deserialize)]
63
+ struct TitanNativeFunc {
64
+ symbol: String,
65
+ #[serde(default)]
66
+ parameters: Vec<String>,
67
+ #[serde(default)]
68
+ result: String,
69
+ }
70
+
71
+ pub fn load_project_extensions(root: PathBuf) {
72
+ let mut modules = Vec::new();
73
+ let mut libs = Vec::new();
74
+ let mut all_natives = Vec::new();
75
+
76
+ let mut node_modules = root.join("node_modules");
77
+ if !node_modules.exists() {
78
+ if let Some(parent) = root.parent() {
79
+ let parent_modules = parent.join("node_modules");
80
+ if parent_modules.exists() {
81
+ node_modules = parent_modules;
82
+ }
83
+ }
84
+ }
85
+
86
+ if node_modules.exists() {
87
+ for entry in WalkDir::new(&node_modules).min_depth(1).max_depth(2) {
88
+ let entry = match entry { Ok(e) => e, Err(_) => continue };
89
+ if entry.file_type().is_file() && entry.file_name() == "titan.json" {
90
+ let dir = entry.path().parent().unwrap();
91
+ let config_content = match fs::read_to_string(entry.path()) {
92
+ Ok(c) => c,
93
+ Err(_) => continue,
94
+ };
95
+ let config: TitanConfig = match serde_json::from_str(&config_content) {
96
+ Ok(c) => c,
97
+ Err(_) => continue,
98
+ };
99
+
100
+ let mut mod_natives_map = HashMap::new();
101
+
102
+ if let Some(native_conf) = config.native {
103
+ let lib_path = dir.join(&native_conf.path);
104
+ unsafe {
105
+ match Library::new(&lib_path) {
106
+ Ok(lib) => {
107
+ for (fn_name, fn_conf) in native_conf.functions {
108
+ let sig = if fn_conf.parameters.len() == 2
109
+ && fn_conf.parameters[0] == "f64"
110
+ && fn_conf.parameters[1] == "f64"
111
+ && fn_conf.result == "f64" {
112
+ Signature::F64TwoArgsRetF64
113
+ } else {
114
+ Signature::Unknown
115
+ };
116
+
117
+ if let Ok(symbol) = lib.get::<*const ()>(fn_conf.symbol.as_bytes()) {
118
+ let idx = all_natives.len();
119
+ all_natives.push(NativeFnEntry {
120
+ ptr: *symbol as usize,
121
+ sig
122
+ });
123
+ mod_natives_map.insert(fn_name, idx);
124
+ }
125
+ }
126
+ libs.push(lib);
127
+ },
128
+ Err(e) => println!("Failed to load extension library {}: {}", lib_path.display(), e),
129
+ }
130
+ }
131
+ }
132
+
133
+ let js_path = dir.join(&config.main);
134
+ let js_content = fs::read_to_string(js_path).unwrap_or_default();
135
+
136
+ modules.push(ModuleDef {
137
+ name: config.name.clone(),
138
+ js: js_content,
139
+ native_indices: mod_natives_map,
140
+ });
141
+
142
+ println!("{} {} {}", blue("[Titan]"), green("Extension loaded:"), config.name);
143
+ }
144
+ }
145
+ }
146
+
147
+ *REGISTRY.lock().unwrap() = Some(Registry { _libs: libs, modules, natives: all_natives });
148
+ }
149
+
14
150
 
15
151
  static V8_INIT: Once = Once::new();
16
152
 
@@ -316,12 +452,63 @@ fn native_define_action(_scope: &mut v8::HandleScope, args: v8::FunctionCallback
316
452
  retval.set(args.get(0));
317
453
  }
318
454
 
455
+ // ----------------------------------------------------------------------------
456
+ // NATIVE CALLBACKS (EXTENSIONS)
457
+ // ----------------------------------------------------------------------------
458
+
459
+ // generic wrappers could go here if needed
460
+
461
+ fn native_invoke_extension(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
462
+ let fn_idx = args.get(0).to_integer(scope).unwrap().value() as usize;
463
+
464
+ // Get pointer from registry
465
+ let mut ptr = 0;
466
+ let mut sig = Signature::Unknown;
467
+
468
+ if let Ok(guard) = REGISTRY.lock() {
469
+ if let Some(registry) = &*guard {
470
+ if let Some(entry) = registry.natives.get(fn_idx) {
471
+ ptr = entry.ptr;
472
+ sig = entry.sig;
473
+ }
474
+ }
475
+ }
476
+
477
+ if ptr == 0 {
478
+ throw(scope, "Native function not found");
479
+ return;
480
+ }
481
+
482
+ match sig {
483
+ Signature::F64TwoArgsRetF64 => {
484
+ let a = args.get(1).to_number(scope).unwrap_or(v8::Number::new(scope, 0.0)).value();
485
+ let b = args.get(2).to_number(scope).unwrap_or(v8::Number::new(scope, 0.0)).value();
486
+
487
+ unsafe {
488
+ let func: extern "C" fn(f64, f64) -> f64 = std::mem::transmute(ptr);
489
+ let res = func(a, b);
490
+ retval.set(v8::Number::new(scope, res).into());
491
+ }
492
+ },
493
+ _ => throw(scope, "Unsupported signature"),
494
+ }
495
+ }
496
+
497
+
319
498
  // ----------------------------------------------------------------------------
320
499
  // INJECTOR
321
500
  // ----------------------------------------------------------------------------
322
501
 
502
+
323
503
  pub fn inject_extensions(scope: &mut v8::HandleScope, global: v8::Local<v8::Object>) {
504
+ // Ensure globalThis reference
505
+ let gt_key = v8_str(scope, "globalThis");
506
+ global.set(scope, gt_key.into(), global.into());
507
+
324
508
  let t_obj = v8::Object::new(scope);
509
+ let t_key = v8_str(scope, "t");
510
+ // Use create_data_property to guarantee definition
511
+ global.create_data_property(scope, t_key.into(), t_obj.into()).unwrap();
325
512
 
326
513
  // defineAction (identity function for clean typing)
327
514
  let def_fn = v8::Function::new(scope, native_define_action).unwrap();
@@ -369,6 +556,80 @@ pub fn inject_extensions(scope: &mut v8::HandleScope, global: v8::Local<v8::Obje
369
556
  let pw_key = v8_str(scope, "password");
370
557
  t_obj.set(scope, pw_key.into(), pw_obj.into());
371
558
 
559
+
560
+ // Inject __titan_invoke_native
561
+ let invoke_fn = v8::Function::new(scope, native_invoke_extension).unwrap();
562
+ let invoke_key = v8_str(scope, "__titan_invoke_native");
563
+ global.set(scope, invoke_key.into(), invoke_fn.into());
564
+
565
+ // Inject Loaded Extensions
566
+ let modules = if let Ok(guard) = REGISTRY.lock() {
567
+ if let Some(registry) = &*guard {
568
+ registry.modules.clone()
569
+ } else {
570
+ Vec::new()
571
+ }
572
+ } else {
573
+ Vec::new()
574
+ };
575
+
576
+ for module in modules {
577
+ let mod_obj = v8::Object::new(scope);
578
+
579
+ // Generate JS wrappers
580
+ for (fn_name, &idx) in &module.native_indices {
581
+ let code = format!("(function(a, b) {{ return __titan_invoke_native({}, a, b); }})", idx);
582
+ let source = v8_str(scope, &code);
583
+ if let Some(script) = v8::Script::compile(scope, source, None) {
584
+ if let Some(val) = script.run(scope) {
585
+ let key = v8_str(scope, fn_name);
586
+ mod_obj.set(scope, key.into(), val);
587
+ }
588
+ }
589
+ }
590
+
591
+ // Inject t.<module_name>
592
+ let mod_key = v8_str(scope, &module.name);
593
+ t_obj.set(scope, mod_key.into(), mod_obj.into());
594
+
595
+ // Set context for logging
596
+ let action_key = v8_str(scope, "__titan_action");
597
+ let action_val = v8_str(scope, &module.name);
598
+ global.set(scope, action_key.into(), action_val.into());
599
+
600
+ // Execute JS
601
+ // Wrap in IIFE passing 't' to ensure visibility
602
+ let wrapped_js = format!("(function(t) {{ {} }})", module.js);
603
+ let source = v8_str(scope, &wrapped_js);
604
+ let tc = &mut v8::TryCatch::new(scope);
605
+
606
+ if let Some(script) = v8::Script::compile(tc, source, None) {
607
+ if let Some(func_val) = script.run(tc) {
608
+ // func_val is the function. Call it with [t_obj]
609
+ if let Ok(func) = v8::Local::<v8::Function>::try_from(func_val) {
610
+ let receiver = v8::undefined(&mut *tc).into();
611
+ let args = [t_obj.into()];
612
+ // Pass tc (which is a scope)
613
+ if func.call(&mut *tc, receiver, &args).is_none() {
614
+ println!("{} {}", crate::utils::blue("[Titan]"), crate::utils::red("Extension Execution Failed"));
615
+ if let Some(msg) = tc.message() {
616
+ let text = msg.get(&mut *tc).to_rust_string_lossy(&mut *tc);
617
+ println!("{} {}", crate::utils::red("Error details:"), text);
618
+ }
619
+ }
620
+ }
621
+ } else {
622
+ let msg = tc.message().unwrap();
623
+ let text = msg.get(&mut *tc).to_rust_string_lossy(&mut *tc);
624
+ println!("{} {} {}", crate::utils::blue("[Titan]"), crate::utils::red("Extension JS Error:"), text);
625
+ }
626
+ } else {
627
+ let msg = tc.message().unwrap();
628
+ let text = msg.get(&mut *tc).to_rust_string_lossy(&mut *tc);
629
+ println!("{} {} {}", crate::utils::blue("[Titan]"), crate::utils::red("Extension Compile Error:"), text);
630
+ }
631
+ }
632
+
372
633
  // t.db (Stub for now)
373
634
  let db_obj = v8::Object::new(scope);
374
635
  let db_key = v8_str(scope, "db");
@@ -156,7 +156,14 @@ async fn dynamic_handler_inner(
156
156
  .or_else(|| find_actions_dir(&state.project_root))
157
157
  .unwrap();
158
158
 
159
- let action_path = actions_dir.join(format!("{}.jsbundle", action_name));
159
+ let mut action_path = actions_dir.join(format!("{}.jsbundle", action_name));
160
+ if !action_path.exists() {
161
+ let js_path = actions_dir.join(format!("{}.js", action_name));
162
+ if js_path.exists() {
163
+ action_path = js_path;
164
+ }
165
+ }
166
+
160
167
  let js_code = match fs::read_to_string(&action_path) {
161
168
  Ok(c) => c,
162
169
  Err(_) => {
@@ -311,10 +318,14 @@ async fn main() -> Result<()> {
311
318
  let state = AppState {
312
319
  routes: Arc::new(map),
313
320
  dynamic_routes: Arc::new(dynamic_routes),
314
- project_root,
321
+ project_root: project_root.clone(),
315
322
  };
323
+
324
+ // Load extensions
325
+ extensions::load_project_extensions(project_root.clone());
316
326
 
317
327
  let app = Router::new()
328
+
318
329
  .route("/", any(root_route))
319
330
  .fallback(any(dynamic_route))
320
331
  .with_state(state);
@@ -2,6 +2,7 @@ import chokidar from "chokidar";
2
2
  import { spawn, execSync } from "child_process";
3
3
  import path from "path";
4
4
  import { fileURLToPath } from "url";
5
+ import fs from "fs";
5
6
  import { bundle } from "./bundle.js";
6
7
 
7
8
  // Required for __dirname in ES modules
@@ -17,8 +18,6 @@ async function killServer() {
17
18
  const killPromise = new Promise((resolve) => {
18
19
  if (serverProcess.exitCode !== null) return resolve();
19
20
  serverProcess.once("close", resolve);
20
- // Fallback timeout in case close never fires?
21
- // usually close fires after kill.
22
21
  });
23
22
 
24
23
  if (process.platform === "win32") {
@@ -68,6 +67,10 @@ async function rebuild() {
68
67
  async function startDev() {
69
68
  console.log("[Titan] Dev mode starting...");
70
69
 
70
+ if (fs.existsSync(path.join(process.cwd(), ".env"))) {
71
+ console.log("\x1b[33m[Titan] Env Configured\x1b[0m");
72
+ }
73
+
71
74
  // FIRST BUILD
72
75
  try {
73
76
  await rebuild();
@@ -76,7 +79,7 @@ async function startDev() {
76
79
  console.log("\x1b[31m[Titan] Initial build failed. Waiting for changes...\x1b[0m");
77
80
  }
78
81
 
79
- const watcher = chokidar.watch("app", {
82
+ const watcher = chokidar.watch(["app", ".env"], {
80
83
  ignoreInitial: true
81
84
  });
82
85
 
@@ -86,7 +89,11 @@ async function startDev() {
86
89
  if (timer) clearTimeout(timer);
87
90
 
88
91
  timer = setTimeout(async () => {
89
- console.log(`[Titan] Change detected: ${file}`);
92
+ if (file.includes(".env")) {
93
+ console.log("\x1b[33m[Titan] Env Refreshed\x1b[0m");
94
+ } else {
95
+ console.log(`[Titan] Change detected: ${file}`);
96
+ }
90
97
 
91
98
  try {
92
99
  await rebuild();