@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 +21 -2
- package/index.js +227 -18
- package/package.json +1 -1
- package/templates/extension/README.md +104 -0
- package/templates/extension/index.js +26 -0
- package/templates/extension/jsconfig.json +13 -0
- package/templates/extension/native/Cargo.toml +9 -0
- package/templates/extension/native/src/lib.rs +5 -0
- package/templates/extension/package.json +19 -0
- package/templates/extension/titan.json +18 -0
- package/templates/server/Cargo.toml +2 -0
- package/templates/server/src/errors.rs +1 -1
- package/templates/server/src/extensions.rs +262 -1
- package/templates/server/src/main.rs +13 -2
- package/templates/titan/dev.js +11 -4
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 (
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
break;
|
|
383
|
-
|
|
384
|
-
|
|
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.
|
|
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,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
|
+
}
|
|
@@ -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);
|
package/templates/titan/dev.js
CHANGED
|
@@ -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
|
-
|
|
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();
|