@ezetgalaxy/titan 26.8.3 → 26.9.1
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 +76 -17
- package/index.js +118 -29
- package/package.json +1 -2
- package/templates/extension/README.md +104 -104
- package/templates/extension/index.js +27 -27
- package/templates/extension/jsconfig.json +12 -12
- package/templates/extension/native/Cargo.toml +9 -9
- package/templates/extension/native/src/lib.rs +5 -5
- package/templates/extension/package.json +20 -20
- package/templates/extension/titan.json +17 -17
- package/templates/js/Dockerfile +66 -66
- package/templates/js/_dockerignore +3 -3
- package/templates/js/_gitignore +1 -0
- package/templates/js/app/actions/hello.js +5 -5
- package/templates/js/app/titan.d.ts +87 -87
- package/templates/js/jsconfig.json +18 -18
- package/templates/js/server/src/action_management.rs +131 -131
- package/templates/js/server/src/errors.rs +10 -10
- package/templates/js/server/src/extensions.rs +989 -989
- package/templates/js/server/src/utils.rs +33 -33
- package/templates/js/titan/bundle.js +78 -78
- package/templates/js/titan/dev.js +9 -1
- package/templates/js/titan/titan.js +122 -122
- package/templates/{rust → rust-js}/Dockerfile +66 -66
- package/templates/{rust → rust-js}/_dockerignore +3 -3
- package/templates/{rust → rust-js}/_gitignore +1 -0
- package/templates/{rust → rust-js}/app/actions/hello.js +5 -5
- package/templates/{rust → rust-js}/app/actions/rust_hello.rs +14 -14
- package/templates/{rust → rust-js}/app/titan.d.ts +101 -101
- package/templates/{rust → rust-js}/jsconfig.json +18 -18
- package/templates/{rust → rust-js}/package.json +1 -1
- package/templates/{rust → rust-js}/server/src/action_management.rs +131 -131
- package/templates/{rust → rust-js}/server/src/errors.rs +10 -10
- package/templates/{rust → rust-js}/server/src/extensions.rs +989 -989
- package/templates/{rust → rust-js}/server/src/utils.rs +33 -33
- package/templates/{rust → rust-js}/titan/dev.js +9 -1
- package/templates/rust-ts/Dockerfile +66 -0
- package/templates/rust-ts/_dockerignore +3 -0
- package/templates/rust-ts/_gitignore +38 -0
- package/templates/rust-ts/app/actions/hello.ts +11 -0
- package/templates/rust-ts/app/actions/rust_hello.rs +14 -0
- package/templates/rust-ts/app/app.ts +11 -0
- package/templates/rust-ts/package.json +14 -0
- package/templates/rust-ts/server/Cargo.lock +2869 -0
- package/templates/rust-ts/server/Cargo.toml +39 -0
- package/templates/rust-ts/server/src/action_management.rs +131 -0
- package/templates/rust-ts/server/src/errors.rs +51 -0
- package/templates/rust-ts/server/src/extensions.rs +989 -0
- package/templates/rust-ts/server/src/main.rs +468 -0
- package/templates/rust-ts/server/src/utils.rs +33 -0
- package/templates/rust-ts/titan/bundle.js +163 -0
- package/templates/rust-ts/titan/dev.js +402 -0
- package/templates/rust-ts/titan/titan.d.ts +117 -0
- package/templates/rust-ts/titan/titan.js +122 -0
- package/templates/rust-ts/tsconfig.json +21 -0
- package/templates/ts/Dockerfile +40 -0
- package/templates/ts/_dockerignore +3 -0
- package/templates/ts/_gitignore +38 -0
- package/templates/ts/app/actions/hello.ts +11 -0
- package/templates/ts/app/app.ts +10 -0
- package/templates/ts/package.json +26 -0
- package/templates/ts/server/Cargo.lock +2869 -0
- package/templates/ts/server/Cargo.toml +27 -0
- package/templates/ts/server/src/action_management.rs +131 -0
- package/templates/ts/server/src/errors.rs +51 -0
- package/templates/ts/server/src/extensions.rs +989 -0
- package/templates/ts/server/src/main.rs +437 -0
- package/templates/ts/server/src/utils.rs +33 -0
- package/templates/ts/titan/builder.js +121 -0
- package/templates/ts/titan/bundle.js +76 -0
- package/templates/ts/titan/dev.js +402 -0
- package/templates/ts/titan/runtime.js +1 -0
- package/templates/ts/titan/titan.d.ts +117 -0
- package/templates/ts/titan/titan.js +122 -0
- package/templates/ts/tsconfig.json +16 -0
- package/titanpl-sdk/README.md +109 -109
- package/titanpl-sdk/bin/run.js +251 -254
- package/titanpl-sdk/index.d.ts +46 -46
- package/titanpl-sdk/index.js +5 -5
- package/titanpl-sdk/package.json +32 -32
- package/titanpl-sdk/templates/.dockerignore +3 -3
- package/titanpl-sdk/templates/Dockerfile +53 -53
- package/titanpl-sdk/templates/app/actions/hello.js +5 -5
- package/titanpl-sdk/templates/app/titan.d.ts +87 -87
- package/titanpl-sdk/templates/jsconfig.json +18 -18
- package/titanpl-sdk/templates/server/src/action_management.rs +131 -131
- package/titanpl-sdk/templates/server/src/errors.rs +10 -10
- package/titanpl-sdk/templates/server/src/extensions.rs +640 -640
- package/titanpl-sdk/templates/server/src/utils.rs +33 -33
- package/titanpl-sdk/templates/titan/bundle.js +65 -65
- package/titanpl-sdk/templates/titan/dev.js +113 -113
- package/titanpl-sdk/templates/titan/titan.js +98 -98
- package/templates/js/server/action_map.json +0 -3
- package/templates/js/server/actions/hello.jsbundle +0 -48
- package/templates/js/server/routes.json +0 -16
- package/templates/rust/server/action_map.json +0 -3
- package/templates/rust/server/actions/hello.jsbundle +0 -47
- package/templates/rust/server/routes.json +0 -22
- package/templates/rust/server/src/actions_rust/mod.rs +0 -19
- package/templates/rust/server/src/actions_rust/rust_hello.rs +0 -14
- /package/templates/{rust → rust-js}/app/app.js +0 -0
- /package/templates/{rust → rust-js}/server/Cargo.lock +0 -0
- /package/templates/{rust → rust-js}/server/Cargo.toml +0 -0
- /package/templates/{rust → rust-js}/server/src/main.rs +0 -0
- /package/templates/{rust → rust-js}/titan/bundle.js +0 -0
- /package/templates/{rust → rust-js}/titan/titan.js +0 -0
|
@@ -0,0 +1,402 @@
|
|
|
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 esbuild from "esbuild";
|
|
7
|
+
import { createRequire } from "module";
|
|
8
|
+
|
|
9
|
+
// Required for __dirname in ES modules
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
// Colors
|
|
15
|
+
const cyan = (t) => `\x1b[36m${t}\x1b[0m`;
|
|
16
|
+
const green = (t) => `\x1b[32m${t}\x1b[0m`;
|
|
17
|
+
const yellow = (t) => `\x1b[33m${t}\x1b[0m`;
|
|
18
|
+
const red = (t) => `\x1b[31m${t}\x1b[0m`;
|
|
19
|
+
const gray = (t) => `\x1b[90m${t}\x1b[0m`;
|
|
20
|
+
const bold = (t) => `\x1b[1m${t}\x1b[0m`;
|
|
21
|
+
|
|
22
|
+
function getTitanVersion() {
|
|
23
|
+
try {
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
const pkgPath = require.resolve("@ezetgalaxy/titan/package.json");
|
|
26
|
+
return JSON.parse(fs.readFileSync(pkgPath, "utf-8")).version;
|
|
27
|
+
} catch (e) {
|
|
28
|
+
try {
|
|
29
|
+
// Check levels up to find the framework root
|
|
30
|
+
let cur = __dirname;
|
|
31
|
+
for (let i = 0; i < 5; i++) {
|
|
32
|
+
const pkgPath = path.join(cur, "package.json");
|
|
33
|
+
if (fs.existsSync(pkgPath)) {
|
|
34
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
35
|
+
if (pkg.name === "@ezetgalaxy/titan") return pkg.version;
|
|
36
|
+
}
|
|
37
|
+
cur = path.join(cur, "..");
|
|
38
|
+
}
|
|
39
|
+
} catch (e2) { }
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
// Fallback to calling tit --version
|
|
43
|
+
const output = execSync("tit --version", { encoding: "utf-8" }).trim();
|
|
44
|
+
const match = output.match(/v(\d+\.\d+\.\d+)/);
|
|
45
|
+
if (match) return match[1];
|
|
46
|
+
} catch (e3) { }
|
|
47
|
+
}
|
|
48
|
+
return "0.1.0";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let serverProcess = null;
|
|
52
|
+
let isKilling = false;
|
|
53
|
+
let isFirstBoot = true;
|
|
54
|
+
|
|
55
|
+
async function killServer() {
|
|
56
|
+
if (!serverProcess) return;
|
|
57
|
+
|
|
58
|
+
isKilling = true;
|
|
59
|
+
const pid = serverProcess.pid;
|
|
60
|
+
const killPromise = new Promise((resolve) => {
|
|
61
|
+
if (serverProcess.exitCode !== null) return resolve();
|
|
62
|
+
serverProcess.once("close", resolve);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (process.platform === "win32") {
|
|
66
|
+
try {
|
|
67
|
+
execSync(`taskkill /pid ${pid} /f /t`, { stdio: 'ignore' });
|
|
68
|
+
} catch (e) {
|
|
69
|
+
// Ignore errors if process is already dead
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
serverProcess.kill();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
await killPromise;
|
|
77
|
+
} catch (e) { }
|
|
78
|
+
serverProcess = null;
|
|
79
|
+
isKilling = false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const delay = (ms) => new Promise(res => setTimeout(res, ms));
|
|
83
|
+
|
|
84
|
+
let spinnerTimer = null;
|
|
85
|
+
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
86
|
+
let frameIdx = 0;
|
|
87
|
+
|
|
88
|
+
function startSpinner(text) {
|
|
89
|
+
if (spinnerTimer) clearInterval(spinnerTimer);
|
|
90
|
+
process.stdout.write("\x1B[?25l"); // Hide cursor
|
|
91
|
+
spinnerTimer = setInterval(() => {
|
|
92
|
+
process.stdout.write(`\r ${cyan(frames[frameIdx])} ${gray(text)}`);
|
|
93
|
+
frameIdx = (frameIdx + 1) % frames.length;
|
|
94
|
+
}, 80);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function stopSpinner(success = true, text = "") {
|
|
98
|
+
if (spinnerTimer) {
|
|
99
|
+
clearInterval(spinnerTimer);
|
|
100
|
+
spinnerTimer = null;
|
|
101
|
+
}
|
|
102
|
+
process.stdout.write("\r\x1B[K"); // Clear line
|
|
103
|
+
process.stdout.write("\x1B[?25h"); // Show cursor
|
|
104
|
+
if (text) {
|
|
105
|
+
if (success) {
|
|
106
|
+
console.log(` ${green("✔")} ${green(text)}`);
|
|
107
|
+
} else {
|
|
108
|
+
console.log(` ${red("✖")} ${red(text)}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function startRustServer(retryCount = 0) {
|
|
114
|
+
// If TS is broken, don't start
|
|
115
|
+
if (isTs && !isTsHealthy) {
|
|
116
|
+
stopSpinner(false, "Waiting for TypeScript errors to be fixed...");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const waitTime = retryCount > 0 ? 1000 : 500;
|
|
121
|
+
|
|
122
|
+
await killServer();
|
|
123
|
+
await delay(waitTime);
|
|
124
|
+
|
|
125
|
+
const serverPath = path.join(process.cwd(), "server");
|
|
126
|
+
const startTime = Date.now();
|
|
127
|
+
|
|
128
|
+
startSpinner("Stabilizing your app on its orbit...");
|
|
129
|
+
|
|
130
|
+
let isReady = false;
|
|
131
|
+
let stdoutBuffer = "";
|
|
132
|
+
let buildLogs = "";
|
|
133
|
+
|
|
134
|
+
// If it takes more than 30s, update the message
|
|
135
|
+
const slowTimer = setTimeout(() => {
|
|
136
|
+
if (!isReady && !isKilling) {
|
|
137
|
+
startSpinner("Still stabilizing... (the first orbit takes longer)");
|
|
138
|
+
}
|
|
139
|
+
}, 30000);
|
|
140
|
+
|
|
141
|
+
serverProcess = spawn("cargo", ["run", "--quiet"], {
|
|
142
|
+
cwd: serverPath,
|
|
143
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
144
|
+
env: { ...process.env, CARGO_INCREMENTAL: "1" }
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
serverProcess.on("error", (err) => {
|
|
148
|
+
stopSpinner(false, "Failed to start orbit");
|
|
149
|
+
console.error(red(`[Titan] Error: ${err.message}`));
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
serverProcess.stderr.on("data", (data) => {
|
|
153
|
+
const str = data.toString();
|
|
154
|
+
if (isReady) {
|
|
155
|
+
process.stderr.write(data);
|
|
156
|
+
} else {
|
|
157
|
+
buildLogs += str;
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
serverProcess.stdout.on("data", (data) => {
|
|
162
|
+
const out = data.toString();
|
|
163
|
+
|
|
164
|
+
if (!isReady) {
|
|
165
|
+
stdoutBuffer += out;
|
|
166
|
+
if (stdoutBuffer.includes("Titan server running") || stdoutBuffer.includes("████████╗")) {
|
|
167
|
+
isReady = true;
|
|
168
|
+
clearTimeout(slowTimer);
|
|
169
|
+
stopSpinner(true, "Your app is now orbiting Titan Planet");
|
|
170
|
+
|
|
171
|
+
if (isFirstBoot) {
|
|
172
|
+
process.stdout.write(stdoutBuffer);
|
|
173
|
+
isFirstBoot = false;
|
|
174
|
+
} else {
|
|
175
|
+
// On subsequent reloads, only print non-banner lines from the buffer
|
|
176
|
+
const lines = stdoutBuffer.split("\n");
|
|
177
|
+
for (const line of lines) {
|
|
178
|
+
const isBanner = line.includes("Titan server running") ||
|
|
179
|
+
line.includes("████████╗") ||
|
|
180
|
+
line.includes("╚══") ||
|
|
181
|
+
line.includes(" ██║") ||
|
|
182
|
+
line.includes(" ╚═╝");
|
|
183
|
+
if (!isBanner && line.trim()) {
|
|
184
|
+
process.stdout.write(line + "\n");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
stdoutBuffer = "";
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
process.stdout.write(data);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
serverProcess.on("close", async (code) => {
|
|
196
|
+
clearTimeout(slowTimer);
|
|
197
|
+
if (isKilling) return;
|
|
198
|
+
const runTime = Date.now() - startTime;
|
|
199
|
+
|
|
200
|
+
if (code !== 0 && code !== null) {
|
|
201
|
+
stopSpinner(false, "Orbit stabilization failed");
|
|
202
|
+
if (!isReady) {
|
|
203
|
+
console.log(gray("\n--- Build Logs ---"));
|
|
204
|
+
console.log(buildLogs);
|
|
205
|
+
console.log(gray("------------------\n"));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (runTime < 15000 && retryCount < 5) {
|
|
209
|
+
await delay(2000);
|
|
210
|
+
await startRustServer(retryCount + 1);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function rebuild() {
|
|
217
|
+
if (isTs && !isTsHealthy) return; // Don't rebuild if TS is broken
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
const root = process.cwd();
|
|
221
|
+
const appTs = path.join(root, "app", "app.ts");
|
|
222
|
+
const dotTitan = path.join(root, ".titan");
|
|
223
|
+
const compiledApp = path.join(dotTitan, "app.js");
|
|
224
|
+
|
|
225
|
+
if (fs.existsSync(appTs)) {
|
|
226
|
+
if (!fs.existsSync(dotTitan)) fs.mkdirSync(dotTitan, { recursive: true });
|
|
227
|
+
|
|
228
|
+
await esbuild.build({
|
|
229
|
+
entryPoints: [appTs],
|
|
230
|
+
outfile: compiledApp,
|
|
231
|
+
bundle: true,
|
|
232
|
+
platform: "node",
|
|
233
|
+
format: "esm",
|
|
234
|
+
external: ["fs", "path", "esbuild", "chokidar", "typescript"],
|
|
235
|
+
logLevel: "silent"
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
execSync(`node "${compiledApp}"`, { stdio: "inherit" });
|
|
239
|
+
} else {
|
|
240
|
+
execSync("node app/app.js", { stdio: "ignore" });
|
|
241
|
+
}
|
|
242
|
+
} catch (e) {
|
|
243
|
+
stopSpinner(false, "Failed to prepare runtime");
|
|
244
|
+
console.log(red(`[Titan] Error: ${e.message}`));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let tsProcess = null;
|
|
249
|
+
let isTsHealthy = false; // STRICT: Assume unhealthy until checked
|
|
250
|
+
|
|
251
|
+
function startTypeChecker() {
|
|
252
|
+
const root = process.cwd();
|
|
253
|
+
if (!fs.existsSync(path.join(root, "tsconfig.json"))) return;
|
|
254
|
+
|
|
255
|
+
let tscPath;
|
|
256
|
+
try {
|
|
257
|
+
const require = createRequire(import.meta.url);
|
|
258
|
+
tscPath = require.resolve("typescript/bin/tsc");
|
|
259
|
+
} catch (e) {
|
|
260
|
+
tscPath = path.join(root, "node_modules", "typescript", "bin", "tsc");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (!fs.existsSync(tscPath)) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const args = [tscPath, "--noEmit", "--watch", "--preserveWatchOutput", "--pretty"];
|
|
268
|
+
|
|
269
|
+
tsProcess = spawn(process.execPath, args, {
|
|
270
|
+
cwd: root,
|
|
271
|
+
stdio: "pipe",
|
|
272
|
+
shell: false
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
tsProcess.stdout.on("data", (data) => {
|
|
276
|
+
const lines = data.toString().split("\n");
|
|
277
|
+
for (const line of lines) {
|
|
278
|
+
if (line.trim().includes("File change detected") || line.trim().includes("Starting compilation")) {
|
|
279
|
+
isTsHealthy = false;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (line.includes("Found 0 errors")) {
|
|
283
|
+
isTsHealthy = true;
|
|
284
|
+
// TS is happy, so we rebuild and restart (or start) the server
|
|
285
|
+
rebuild().then(startRustServer);
|
|
286
|
+
|
|
287
|
+
} else if (line.includes("error TS")) {
|
|
288
|
+
isTsHealthy = false;
|
|
289
|
+
if (serverProcess) {
|
|
290
|
+
console.log(red(`[Titan] TypeScript error detected. Stopping server...`));
|
|
291
|
+
killServer();
|
|
292
|
+
}
|
|
293
|
+
process.stdout.write(line + "\n");
|
|
294
|
+
} else if (line.match(/Found [1-9]\d* error/)) {
|
|
295
|
+
isTsHealthy = false;
|
|
296
|
+
if (serverProcess) {
|
|
297
|
+
console.log(red(`[Titan] TypeScript compilation failed. Stopping server...`));
|
|
298
|
+
killServer();
|
|
299
|
+
}
|
|
300
|
+
process.stdout.write(line + "\n");
|
|
301
|
+
} else if (line.trim()) {
|
|
302
|
+
process.stdout.write(gray(`[TS] ${line}\n`));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
tsProcess.stderr.on("data", (data) => {
|
|
308
|
+
process.stdout.write(data);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
let isTs = false;
|
|
313
|
+
|
|
314
|
+
async function startDev() {
|
|
315
|
+
const root = process.cwd();
|
|
316
|
+
const actionsDir = path.join(root, "app", "actions");
|
|
317
|
+
let hasRust = false;
|
|
318
|
+
if (fs.existsSync(actionsDir)) {
|
|
319
|
+
hasRust = fs.readdirSync(actionsDir).some(f => f.endsWith(".rs"));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
isTs = fs.existsSync(path.join(root, "tsconfig.json")) ||
|
|
323
|
+
fs.existsSync(path.join(root, "app", "app.ts"));
|
|
324
|
+
|
|
325
|
+
let mode = "";
|
|
326
|
+
if (hasRust) {
|
|
327
|
+
mode = isTs ? "Rust + TS Actions" : "Rust + JS Actions";
|
|
328
|
+
} else {
|
|
329
|
+
mode = isTs ? "TS Actions" : "JS Actions";
|
|
330
|
+
}
|
|
331
|
+
const version = getTitanVersion();
|
|
332
|
+
|
|
333
|
+
console.clear();
|
|
334
|
+
console.log("");
|
|
335
|
+
console.log(` ${bold(cyan("Titan Planet"))} ${gray("v" + version)} ${yellow("[ Dev Mode ]")}`);
|
|
336
|
+
console.log("");
|
|
337
|
+
console.log(` ${gray("Type: ")} ${mode}`);
|
|
338
|
+
console.log(` ${gray("Hot Reload: ")} ${green("Enabled")}`);
|
|
339
|
+
|
|
340
|
+
if (fs.existsSync(path.join(root, ".env"))) {
|
|
341
|
+
console.log(` ${gray("Env: ")} ${yellow("Loaded")}`);
|
|
342
|
+
}
|
|
343
|
+
console.log("");
|
|
344
|
+
|
|
345
|
+
if (isTs) {
|
|
346
|
+
startTypeChecker();
|
|
347
|
+
} else {
|
|
348
|
+
// If no TS, start immediately
|
|
349
|
+
try {
|
|
350
|
+
await rebuild();
|
|
351
|
+
await startRustServer();
|
|
352
|
+
} catch (e) {
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const watcher = chokidar.watch(["app", ".env"], {
|
|
357
|
+
ignoreInitial: true,
|
|
358
|
+
awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 }
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
let timer = null;
|
|
362
|
+
watcher.on("all", async (event, file) => {
|
|
363
|
+
if (timer) clearTimeout(timer);
|
|
364
|
+
timer = setTimeout(async () => {
|
|
365
|
+
// If TS, we rely on TCS to trigger the rebuild (via Found 0 errors)
|
|
366
|
+
// We verify path safety using absolute/relative calculations
|
|
367
|
+
const relPath = path.relative(root, file);
|
|
368
|
+
if (isTs && (relPath.startsWith("app") || relPath.startsWith("app" + path.sep))) return;
|
|
369
|
+
|
|
370
|
+
// If TS is broken, rebuild() checks will prevent update, keeping server dead
|
|
371
|
+
// If TS is healthy, we proceed
|
|
372
|
+
if (isTs && !isTsHealthy) return;
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
await killServer();
|
|
376
|
+
await rebuild();
|
|
377
|
+
await startRustServer();
|
|
378
|
+
} catch (e) {
|
|
379
|
+
// console.log(red("[Titan] Build failed -- waiting for changes..."));
|
|
380
|
+
}
|
|
381
|
+
}, 300);
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function handleExit() {
|
|
386
|
+
stopSpinner();
|
|
387
|
+
console.log(gray("\n[Titan] Stopping server..."));
|
|
388
|
+
await killServer();
|
|
389
|
+
if (tsProcess) {
|
|
390
|
+
if (process.platform === "win32") {
|
|
391
|
+
try { execSync(`taskkill /pid ${tsProcess.pid} /f /t`, { stdio: 'ignore' }); } catch (e) { }
|
|
392
|
+
} else {
|
|
393
|
+
tsProcess.kill();
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
process.exit(0);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
process.on("SIGINT", handleExit);
|
|
400
|
+
process.on("SIGTERM", handleExit);
|
|
401
|
+
|
|
402
|
+
startDev();
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
|
|
2
|
+
// -- Module Definitions (for imports from "titan") --
|
|
3
|
+
|
|
4
|
+
export interface RouteHandler {
|
|
5
|
+
reply(value: any): void;
|
|
6
|
+
action(name: string): void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface TitanBuilder {
|
|
10
|
+
get(route: string): RouteHandler;
|
|
11
|
+
post(route: string): RouteHandler;
|
|
12
|
+
log(module: string, msg: string): void;
|
|
13
|
+
start(port?: number, msg?: string): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// The default export from titan.js is the Builder
|
|
17
|
+
declare const builder: TitanBuilder;
|
|
18
|
+
export default builder;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Define a Titan Action with type inference.
|
|
22
|
+
*/
|
|
23
|
+
export declare function defineAction<T>(actionFn: (req: TitanRequest) => T): (req: TitanRequest) => T;
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
// -- Global Definitions (Runtime Environment) --
|
|
27
|
+
|
|
28
|
+
declare global {
|
|
29
|
+
/**
|
|
30
|
+
* The Titan Request Object passed to actions.
|
|
31
|
+
*/
|
|
32
|
+
interface TitanRequest {
|
|
33
|
+
body: any;
|
|
34
|
+
method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
|
|
35
|
+
path: string;
|
|
36
|
+
headers: {
|
|
37
|
+
host?: string;
|
|
38
|
+
"content-type"?: string;
|
|
39
|
+
"user-agent"?: string;
|
|
40
|
+
authorization?: string;
|
|
41
|
+
[key: string]: string | undefined;
|
|
42
|
+
};
|
|
43
|
+
params: Record<string, string>;
|
|
44
|
+
query: Record<string, string>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface DbConnection {
|
|
48
|
+
/**
|
|
49
|
+
* Execute a SQL query.
|
|
50
|
+
* @param sql The SQL query string.
|
|
51
|
+
* @param params (Optional) Parameters for the query ($1, $2, etc).
|
|
52
|
+
*/
|
|
53
|
+
query(sql: string, params?: any[]): any[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Global defineAction (available without import in runtime, though imports are preferred in TS)
|
|
58
|
+
*/
|
|
59
|
+
function defineAction<T>(actionFn: (req: TitanRequest) => T): (req: TitanRequest) => T;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Global Request Object
|
|
63
|
+
* Available automatically in actions.
|
|
64
|
+
*/
|
|
65
|
+
var req: TitanRequest;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Titan Runtime Utilities
|
|
69
|
+
* (Available globally in the runtime, e.g. inside actions)
|
|
70
|
+
*/
|
|
71
|
+
const t: {
|
|
72
|
+
/**
|
|
73
|
+
* Log messages to the server console with Titan formatting.
|
|
74
|
+
*/
|
|
75
|
+
log(...args: any[]): void;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Read a file contents as string.
|
|
79
|
+
* @param path Relative path to the file from project root.
|
|
80
|
+
*/
|
|
81
|
+
read(path: string): string;
|
|
82
|
+
|
|
83
|
+
fetch(url: string, options?: {
|
|
84
|
+
method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
|
|
85
|
+
headers?: Record<string, string>;
|
|
86
|
+
body?: string | object;
|
|
87
|
+
}): {
|
|
88
|
+
ok: boolean;
|
|
89
|
+
status?: number;
|
|
90
|
+
body?: string;
|
|
91
|
+
error?: string;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
jwt: {
|
|
95
|
+
sign(
|
|
96
|
+
payload: object,
|
|
97
|
+
secret: string,
|
|
98
|
+
options?: { expiresIn?: string | number }
|
|
99
|
+
): string;
|
|
100
|
+
verify(token: string, secret: string): any;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
password: {
|
|
104
|
+
hash(password: string): string;
|
|
105
|
+
verify(password: string, hash: string): boolean;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
db: {
|
|
109
|
+
connect(url: string): DbConnection;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Titan Validator (Zod-compatible)
|
|
114
|
+
*/
|
|
115
|
+
valid: any;
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { bundle } from "./bundle.js";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
|
|
5
|
+
export const defineAction = (handler) => handler;
|
|
6
|
+
|
|
7
|
+
const cyan = (t) => `\x1b[36m${t}\x1b[0m`;
|
|
8
|
+
const green = (t) => `\x1b[32m${t}\x1b[0m`;
|
|
9
|
+
|
|
10
|
+
const routes = {};
|
|
11
|
+
const dynamicRoutes = {};
|
|
12
|
+
const actionMap = {};
|
|
13
|
+
|
|
14
|
+
function addRoute(method, route) {
|
|
15
|
+
const key = `${method.toUpperCase()}:${route}`;
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
reply(value) {
|
|
19
|
+
routes[key] = {
|
|
20
|
+
type: typeof value === "object" ? "json" : "text",
|
|
21
|
+
value
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
action(name) {
|
|
26
|
+
if (route.includes(":")) {
|
|
27
|
+
if (!dynamicRoutes[method]) dynamicRoutes[method] = [];
|
|
28
|
+
dynamicRoutes[method].push({
|
|
29
|
+
method: method.toUpperCase(),
|
|
30
|
+
pattern: route,
|
|
31
|
+
action: name
|
|
32
|
+
});
|
|
33
|
+
} else {
|
|
34
|
+
routes[key] = {
|
|
35
|
+
type: "action",
|
|
36
|
+
value: name
|
|
37
|
+
};
|
|
38
|
+
actionMap[key] = name;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {Object} RouteHandler
|
|
46
|
+
* @property {(value: any) => void} reply - Send a direct response
|
|
47
|
+
* @property {(name: string) => void} action - Bind to a server-side action
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Titan App Builder
|
|
52
|
+
*/
|
|
53
|
+
const t = {
|
|
54
|
+
/**
|
|
55
|
+
* Define a GET route
|
|
56
|
+
* @param {string} route
|
|
57
|
+
* @returns {RouteHandler}
|
|
58
|
+
*/
|
|
59
|
+
get(route) {
|
|
60
|
+
return addRoute("GET", route);
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Define a POST route
|
|
65
|
+
* @param {string} route
|
|
66
|
+
* @returns {RouteHandler}
|
|
67
|
+
*/
|
|
68
|
+
post(route) {
|
|
69
|
+
return addRoute("POST", route);
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
log(module, msg) {
|
|
73
|
+
console.log(`[\x1b[35m${module}\x1b[0m] ${msg}`);
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Start the Titan Server
|
|
78
|
+
* @param {number} [port=3000]
|
|
79
|
+
* @param {string} [msg=""]
|
|
80
|
+
*/
|
|
81
|
+
async start(port = 3000, msg = "") {
|
|
82
|
+
try {
|
|
83
|
+
console.log(cyan("[Titan] Preparing runtime..."));
|
|
84
|
+
await bundle();
|
|
85
|
+
|
|
86
|
+
const base = path.join(process.cwd(), "server");
|
|
87
|
+
if (!fs.existsSync(base)) {
|
|
88
|
+
fs.mkdirSync(base, { recursive: true });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const routesPath = path.join(base, "routes.json");
|
|
92
|
+
const actionMapPath = path.join(base, "action_map.json");
|
|
93
|
+
|
|
94
|
+
fs.writeFileSync(
|
|
95
|
+
routesPath,
|
|
96
|
+
JSON.stringify(
|
|
97
|
+
{
|
|
98
|
+
__config: { port },
|
|
99
|
+
routes,
|
|
100
|
+
__dynamic_routes: Object.values(dynamicRoutes).flat()
|
|
101
|
+
},
|
|
102
|
+
null,
|
|
103
|
+
2
|
|
104
|
+
)
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
fs.writeFileSync(
|
|
108
|
+
actionMapPath,
|
|
109
|
+
JSON.stringify(actionMap, null, 2)
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
console.log(green("✔ Titan metadata written successfully"));
|
|
113
|
+
if (msg) console.log(cyan(msg));
|
|
114
|
+
|
|
115
|
+
} catch (e) {
|
|
116
|
+
console.error(`\x1b[31m[Titan] Build Error: ${e.message}\x1b[0m`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export default t;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"forceConsistentCasingInFileNames": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"baseUrl": ".",
|
|
11
|
+
"paths": {
|
|
12
|
+
"*": [
|
|
13
|
+
"*"
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"include": [
|
|
18
|
+
"app/**/*",
|
|
19
|
+
"titan/**/*"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# ================================================================
|
|
2
|
+
# STAGE 1 — Build
|
|
3
|
+
# ================================================================
|
|
4
|
+
FROM node:20-slim AS builder
|
|
5
|
+
|
|
6
|
+
# Install Titan CLI (latest)
|
|
7
|
+
RUN npm install -g @ezetgalaxy/titan@latest
|
|
8
|
+
|
|
9
|
+
WORKDIR /app
|
|
10
|
+
|
|
11
|
+
# Copy project files
|
|
12
|
+
COPY . .
|
|
13
|
+
|
|
14
|
+
# Install JS dependencies
|
|
15
|
+
RUN npm install
|
|
16
|
+
|
|
17
|
+
# Build Titan App
|
|
18
|
+
RUN titan build
|
|
19
|
+
|
|
20
|
+
# ================================================================
|
|
21
|
+
# STAGE 2 — Runtime
|
|
22
|
+
# ================================================================
|
|
23
|
+
FROM node:20-slim
|
|
24
|
+
|
|
25
|
+
WORKDIR /app
|
|
26
|
+
|
|
27
|
+
# Copy Titan Build Artifacts
|
|
28
|
+
COPY --from=builder /app/.titan ./.titan
|
|
29
|
+
COPY --from=builder /app/server ./server
|
|
30
|
+
COPY --from=builder /app/package.json ./package.json
|
|
31
|
+
|
|
32
|
+
# Install dependencies (production only, but for now we might need all if not careful)
|
|
33
|
+
# Actually, the runtime depends on `express`-like behavior?
|
|
34
|
+
# No, Titan pure TS runs via `node .titan/app.js` which might need deps?
|
|
35
|
+
# The user's package.json has dependencies.
|
|
36
|
+
RUN npm install --omit=dev
|
|
37
|
+
|
|
38
|
+
EXPOSE 3000
|
|
39
|
+
|
|
40
|
+
CMD ["node", ".titan/app.js"]
|