@t8n/ui 1.0.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 +276 -0
- package/index.d.ts +65 -0
- package/index.js +131 -0
- package/jsconfig.json +13 -0
- package/package.json +32 -0
- package/test-app/.dockerignore +3 -0
- package/test-app/Dockerfile +66 -0
- package/test-app/app/actions/hello.js +7 -0
- package/test-app/app/app.js +10 -0
- package/test-app/app/static/app.html +9 -0
- package/test-app/app/static/styles.css +9 -0
- package/test-app/app/titan.d.ts +249 -0
- package/test-app/eslint.config.js +8 -0
- package/test-app/jsconfig.json +20 -0
- package/test-app/package.json +25 -0
- package/test-app/server/Cargo.toml +32 -0
- package/test-app/server/src/action_management.rs +171 -0
- package/test-app/server/src/errors.rs +10 -0
- package/test-app/server/src/extensions/builtin.rs +828 -0
- package/test-app/server/src/extensions/external.rs +309 -0
- package/test-app/server/src/extensions/mod.rs +430 -0
- package/test-app/server/src/extensions/titan_core.js +178 -0
- package/test-app/server/src/main.rs +433 -0
- package/test-app/server/src/runtime.rs +314 -0
- package/test-app/server/src/utils.rs +33 -0
- package/test-app/server/titan_storage.json +5 -0
- package/test-app/titan/bundle.js +264 -0
- package/test-app/titan/dev.js +350 -0
- package/test-app/titan/error-box.js +268 -0
- package/test-app/titan/titan.js +129 -0
- package/titan.json +20 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev.js
|
|
3
|
+
* Titan development server with hot reload
|
|
4
|
+
* RULE: This file shows ONLY clean error messages - no raw logs, no stack traces
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import chokidar from "chokidar";
|
|
8
|
+
import { spawn, execSync } from "child_process";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import { createRequire } from "module";
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = path.dirname(__filename);
|
|
16
|
+
|
|
17
|
+
// Premium colors
|
|
18
|
+
const cyan = (t) => `\x1b[36m${t}\x1b[0m`;
|
|
19
|
+
const green = (t) => `\x1b[32m${t}\x1b[0m`;
|
|
20
|
+
const yellow = (t) => `\x1b[33m${t}\x1b[0m`;
|
|
21
|
+
const red = (t) => `\x1b[31m${t}\x1b[0m`;
|
|
22
|
+
const gray = (t) => `\x1b[90m${t}\x1b[0m`;
|
|
23
|
+
const bold = (t) => `\x1b[1m${t}\x1b[0m`;
|
|
24
|
+
|
|
25
|
+
function getTitanVersion() {
|
|
26
|
+
try {
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
28
|
+
const pkgPath = require.resolve("@ezetgalaxy/titan/package.json");
|
|
29
|
+
return JSON.parse(fs.readFileSync(pkgPath, "utf-8")).version;
|
|
30
|
+
} catch (e) {
|
|
31
|
+
try {
|
|
32
|
+
let cur = __dirname;
|
|
33
|
+
for (let i = 0; i < 5; i++) {
|
|
34
|
+
const pkgPath = path.join(cur, "package.json");
|
|
35
|
+
if (fs.existsSync(pkgPath)) {
|
|
36
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
37
|
+
if (pkg.name === "@ezetgalaxy/titan") return pkg.version;
|
|
38
|
+
}
|
|
39
|
+
cur = path.join(cur, "..");
|
|
40
|
+
}
|
|
41
|
+
} catch (e2) { }
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const output = execSync("tit --version", { encoding: "utf-8" }).trim();
|
|
45
|
+
const match = output.match(/v(\d+\.\d+\.\d+)/);
|
|
46
|
+
if (match) return match[1];
|
|
47
|
+
} catch (e3) { }
|
|
48
|
+
}
|
|
49
|
+
return "0.1.0";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let serverProcess = null;
|
|
53
|
+
let isKilling = false;
|
|
54
|
+
let isFirstBoot = true;
|
|
55
|
+
|
|
56
|
+
async function killServer() {
|
|
57
|
+
if (!serverProcess) return;
|
|
58
|
+
|
|
59
|
+
isKilling = true;
|
|
60
|
+
const pid = serverProcess.pid;
|
|
61
|
+
const killPromise = new Promise((resolve) => {
|
|
62
|
+
if (serverProcess.exitCode !== null) return resolve();
|
|
63
|
+
serverProcess.once("close", resolve);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (process.platform === "win32") {
|
|
67
|
+
try {
|
|
68
|
+
execSync(`taskkill /pid ${pid} /f /t`, { stdio: 'ignore' });
|
|
69
|
+
} catch (e) {
|
|
70
|
+
// Ignore errors if process is already dead
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
serverProcess.kill();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
await killPromise;
|
|
78
|
+
} catch (e) { }
|
|
79
|
+
serverProcess = null;
|
|
80
|
+
isKilling = false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const delay = (ms) => new Promise(res => setTimeout(res, ms));
|
|
84
|
+
|
|
85
|
+
let spinnerTimer = null;
|
|
86
|
+
const frames = ["⏣", "⟐", "⟡", "⟠", "⟡", "⟐"];
|
|
87
|
+
let frameIdx = 0;
|
|
88
|
+
|
|
89
|
+
function startSpinner(text) {
|
|
90
|
+
if (spinnerTimer) clearInterval(spinnerTimer);
|
|
91
|
+
process.stdout.write("\x1B[?25l"); // Hide cursor
|
|
92
|
+
spinnerTimer = setInterval(() => {
|
|
93
|
+
process.stdout.write(`\r ${cyan(frames[frameIdx])} ${gray(text)}`);
|
|
94
|
+
frameIdx = (frameIdx + 1) % frames.length;
|
|
95
|
+
}, 80);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function stopSpinner(success = true, text = "") {
|
|
99
|
+
if (spinnerTimer) {
|
|
100
|
+
clearInterval(spinnerTimer);
|
|
101
|
+
spinnerTimer = null;
|
|
102
|
+
}
|
|
103
|
+
process.stdout.write("\r\x1B[K"); // Clear line
|
|
104
|
+
process.stdout.write("\x1B[?25h"); // Show cursor
|
|
105
|
+
if (text) {
|
|
106
|
+
if (success) {
|
|
107
|
+
console.log(` ${green("✔")} ${green(text)}`);
|
|
108
|
+
} else {
|
|
109
|
+
console.log(` ${red("✖")} ${red(text)}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function startRustServer(retryCount = 0) {
|
|
115
|
+
const maxRetries = 3;
|
|
116
|
+
const waitTime = retryCount > 0 ? 500 : 200;
|
|
117
|
+
|
|
118
|
+
await killServer();
|
|
119
|
+
await delay(waitTime);
|
|
120
|
+
|
|
121
|
+
const serverPath = path.join(process.cwd(), "server");
|
|
122
|
+
const startTime = Date.now();
|
|
123
|
+
|
|
124
|
+
startSpinner("Stabilizing your app on its orbit...");
|
|
125
|
+
|
|
126
|
+
let isReady = false;
|
|
127
|
+
let stdoutBuffer = "";
|
|
128
|
+
let stderrBuffer = "";
|
|
129
|
+
|
|
130
|
+
const slowTimer = setTimeout(() => {
|
|
131
|
+
if (!isReady && !isKilling) {
|
|
132
|
+
startSpinner("Still stabilizing... (the first orbit takes longer)");
|
|
133
|
+
}
|
|
134
|
+
}, 15000);
|
|
135
|
+
|
|
136
|
+
serverProcess = spawn("cargo", ["run", "--quiet"], {
|
|
137
|
+
cwd: serverPath,
|
|
138
|
+
stdio: ["ignore", "pipe", "pipe"], // Capture stderr to detect port conflicts
|
|
139
|
+
env: { ...process.env, CARGO_INCREMENTAL: "1" }
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
serverProcess.on("error", (err) => {
|
|
143
|
+
stopSpinner(false, "Orbit stabilization failed");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
serverProcess.stdout.on("data", (data) => {
|
|
147
|
+
const out = data.toString();
|
|
148
|
+
|
|
149
|
+
if (!isReady) {
|
|
150
|
+
stdoutBuffer += out;
|
|
151
|
+
if (stdoutBuffer.includes("Titan server running") || stdoutBuffer.includes("████████╗")) {
|
|
152
|
+
isReady = true;
|
|
153
|
+
clearTimeout(slowTimer);
|
|
154
|
+
stopSpinner(true, "Your app is now orbiting Titan Planet");
|
|
155
|
+
|
|
156
|
+
if (isFirstBoot) {
|
|
157
|
+
process.stdout.write(stdoutBuffer);
|
|
158
|
+
isFirstBoot = false;
|
|
159
|
+
} else {
|
|
160
|
+
const lines = stdoutBuffer.split("\n");
|
|
161
|
+
for (const line of lines) {
|
|
162
|
+
const isBanner = line.includes("Titan server running") ||
|
|
163
|
+
line.includes("████████╗") ||
|
|
164
|
+
line.includes("╚══") ||
|
|
165
|
+
line.includes(" ██║") ||
|
|
166
|
+
line.includes(" ╚═╝");
|
|
167
|
+
if (!isBanner && line.trim()) {
|
|
168
|
+
process.stdout.write(line + "\n");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
stdoutBuffer = "";
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
process.stdout.write(data);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Monitor stderr for port binding errors
|
|
180
|
+
serverProcess.stderr.on("data", (data) => {
|
|
181
|
+
stderrBuffer += data.toString();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
serverProcess.on("close", async (code) => {
|
|
185
|
+
clearTimeout(slowTimer);
|
|
186
|
+
if (isKilling) return;
|
|
187
|
+
const runTime = Date.now() - startTime;
|
|
188
|
+
|
|
189
|
+
if (code !== 0 && code !== null) {
|
|
190
|
+
// Check for port binding errors
|
|
191
|
+
const isPortError = stderrBuffer.includes("Address already in use") ||
|
|
192
|
+
stderrBuffer.includes("address in use") ||
|
|
193
|
+
stderrBuffer.includes("os error 10048") || // Windows
|
|
194
|
+
stderrBuffer.includes("EADDRINUSE") ||
|
|
195
|
+
stderrBuffer.includes("AddrInUse");
|
|
196
|
+
|
|
197
|
+
if (isPortError) {
|
|
198
|
+
stopSpinner(false, "Orbit stabilization failed");
|
|
199
|
+
console.log("");
|
|
200
|
+
|
|
201
|
+
console.log(red("⏣ Your application cannot enter this orbit"));
|
|
202
|
+
console.log(red("↳ Another application is already bound to this port."));
|
|
203
|
+
console.log("");
|
|
204
|
+
|
|
205
|
+
console.log(yellow("Recommended Actions:"));
|
|
206
|
+
console.log(yellow(" 1.") + " Release the occupied orbit (stop the other service).");
|
|
207
|
+
console.log(yellow(" 2.") + " Assign your application to a new orbit in " + cyan("app/app.js"));
|
|
208
|
+
console.log(yellow(" Example: ") + cyan('t.start(3001, "Titan Running!")'));
|
|
209
|
+
console.log("");
|
|
210
|
+
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
stopSpinner(false, "Orbit stabilization failed");
|
|
216
|
+
|
|
217
|
+
// // Debug: Show stderr if it's not empty and not a port error
|
|
218
|
+
// if (stderrBuffer && stderrBuffer.trim()) {
|
|
219
|
+
// console.log(gray("\n[Debug] Cargo stderr:"));
|
|
220
|
+
// console.log(gray(stderrBuffer.substring(0, 500))); // Show first 500 chars
|
|
221
|
+
// }
|
|
222
|
+
|
|
223
|
+
if (runTime < 15000 && retryCount < maxRetries) {
|
|
224
|
+
await delay(2000);
|
|
225
|
+
await startRustServer(retryCount + 1);
|
|
226
|
+
} else if (retryCount >= maxRetries) {
|
|
227
|
+
console.log(gray("\n[Titan] Waiting for changes to retry..."));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Rebuild JS runtime
|
|
235
|
+
* RULE: Only show "✖ Runtime preparation failed" on error
|
|
236
|
+
* RULE: No raw logs, no stack traces, no console.error output
|
|
237
|
+
*/
|
|
238
|
+
async function rebuild() {
|
|
239
|
+
try {
|
|
240
|
+
// Execute app.js - pipe both stdout and stderr to capture and filter
|
|
241
|
+
const result = execSync("node app/app.js", {
|
|
242
|
+
encoding: "utf-8",
|
|
243
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// If succeeded, just print stdout (usually empty unless successful logs)
|
|
247
|
+
if (result) process.stdout.write(result);
|
|
248
|
+
} catch (e) {
|
|
249
|
+
stopSpinner(false, "Runtime preparation failed");
|
|
250
|
+
|
|
251
|
+
// RULE: Search for the error box in the output and print ONLY that
|
|
252
|
+
// This removes Node.js version, stack traces, etc.
|
|
253
|
+
const output = (e.stdout || "") + (e.stderr || "");
|
|
254
|
+
|
|
255
|
+
// Find the box content - look for the start border (accounting for ANSI color)
|
|
256
|
+
// Match from the first ┌ up to the last ┘
|
|
257
|
+
const startIdx = output.indexOf('┌');
|
|
258
|
+
const endIdx = output.lastIndexOf('┘');
|
|
259
|
+
|
|
260
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
261
|
+
// Include potential ANSI codes before/after borders
|
|
262
|
+
let box = output.substring(startIdx - 5, endIdx + 1 + 5);
|
|
263
|
+
// Clean up to ensure we start/end at ANSI boundaries or borders
|
|
264
|
+
const realStart = box.indexOf('\x1b[31m┌');
|
|
265
|
+
const realEnd = box.lastIndexOf('┘\x1b[0m');
|
|
266
|
+
|
|
267
|
+
if (realStart !== -1 && realEnd !== -1) {
|
|
268
|
+
console.error("\n" + box.substring(realStart, realEnd + 5) + "\n");
|
|
269
|
+
} else {
|
|
270
|
+
// Fallback to simpler match
|
|
271
|
+
const simpleBox = output.substring(startIdx, endIdx + 1);
|
|
272
|
+
console.error("\n" + red(simpleBox) + "\n");
|
|
273
|
+
}
|
|
274
|
+
} else if (e.stderr && !e.stderr.includes("Node.js v")) {
|
|
275
|
+
console.error(red(e.stderr.trim()));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
throw e;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function startDev() {
|
|
283
|
+
const root = process.cwd();
|
|
284
|
+
const actionsDir = path.join(root, "app", "actions");
|
|
285
|
+
let hasRust = false;
|
|
286
|
+
if (fs.existsSync(actionsDir)) {
|
|
287
|
+
hasRust = fs.readdirSync(actionsDir).some(f => f.endsWith(".rs"));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const isTs = fs.existsSync(path.join(root, "tsconfig.json")) ||
|
|
291
|
+
fs.existsSync(path.join(root, "app", "app.ts"));
|
|
292
|
+
|
|
293
|
+
let mode = "";
|
|
294
|
+
if (hasRust) {
|
|
295
|
+
mode = isTs ? "Rust + TS Actions" : "Rust + JS Actions";
|
|
296
|
+
} else {
|
|
297
|
+
mode = isTs ? "TS Actions" : "JS Actions";
|
|
298
|
+
}
|
|
299
|
+
const version = getTitanVersion();
|
|
300
|
+
|
|
301
|
+
console.clear();
|
|
302
|
+
console.log("");
|
|
303
|
+
console.log(` ${bold(cyan("⏣ Titan Planet"))} ${gray("v" + version)} ${yellow("[ Dev Mode ]")}`);
|
|
304
|
+
console.log("");
|
|
305
|
+
console.log(` ${gray("Type: ")} ${mode}`);
|
|
306
|
+
console.log(` ${gray("Hot Reload: ")} ${green("Enabled")}`);
|
|
307
|
+
|
|
308
|
+
if (fs.existsSync(path.join(root, ".env"))) {
|
|
309
|
+
console.log(` ${gray("Env: ")} ${yellow("Loaded")}`);
|
|
310
|
+
}
|
|
311
|
+
console.log("");
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
await rebuild();
|
|
315
|
+
await startRustServer();
|
|
316
|
+
} catch (e) {
|
|
317
|
+
console.log(gray("\n[Titan] Waiting for changes to retry..."));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const watcher = chokidar.watch(["app", ".env"], {
|
|
321
|
+
ignoreInitial: true,
|
|
322
|
+
awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 }
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
let timer = null;
|
|
326
|
+
watcher.on("all", async (event, file) => {
|
|
327
|
+
if (timer) clearTimeout(timer);
|
|
328
|
+
timer = setTimeout(async () => {
|
|
329
|
+
try {
|
|
330
|
+
await killServer();
|
|
331
|
+
await rebuild();
|
|
332
|
+
await startRustServer();
|
|
333
|
+
} catch (e) {
|
|
334
|
+
console.log(gray("\n[Titan] Waiting for changes to retry..."));
|
|
335
|
+
}
|
|
336
|
+
}, 300);
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function handleExit() {
|
|
341
|
+
stopSpinner();
|
|
342
|
+
console.log(gray("\n[Titan] Stopping server..."));
|
|
343
|
+
await killServer();
|
|
344
|
+
process.exit(0);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
process.on("SIGINT", handleExit);
|
|
348
|
+
process.on("SIGTERM", handleExit);
|
|
349
|
+
|
|
350
|
+
startDev();
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Box Renderer
|
|
3
|
+
* Renders errors in a Next.js-style red terminal box
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Simple color function using ANSI escape codes
|
|
7
|
+
const red = (text) => `\x1b[31m${text}\x1b[0m`;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Wraps text to fit within a specified width
|
|
11
|
+
* @param {string} text - Text to wrap
|
|
12
|
+
* @param {number} maxWidth - Maximum width per line
|
|
13
|
+
* @returns {string[]} Array of wrapped lines
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
function getTitanVersion() {
|
|
17
|
+
try {
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
const pkgPath = require.resolve("@ezetgalaxy/titan/package.json");
|
|
20
|
+
return JSON.parse(fs.readFileSync(pkgPath, "utf-8")).version;
|
|
21
|
+
} catch (e) {
|
|
22
|
+
try {
|
|
23
|
+
let cur = __dirname;
|
|
24
|
+
for (let i = 0; i < 5; i++) {
|
|
25
|
+
const pkgPath = path.join(cur, "package.json");
|
|
26
|
+
if (fs.existsSync(pkgPath)) {
|
|
27
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
28
|
+
if (pkg.name === "@ezetgalaxy/titan") return pkg.version;
|
|
29
|
+
}
|
|
30
|
+
cur = path.join(cur, "..");
|
|
31
|
+
}
|
|
32
|
+
} catch (e2) { }
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const output = execSync("tit --version", { encoding: "utf-8" }).trim();
|
|
36
|
+
const match = output.match(/v(\d+\.\d+\.\d+)/);
|
|
37
|
+
if (match) return match[1];
|
|
38
|
+
} catch (e3) { }
|
|
39
|
+
}
|
|
40
|
+
return "0.1.0";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function wrapText(text, maxWidth) {
|
|
44
|
+
if (!text) return [''];
|
|
45
|
+
|
|
46
|
+
const lines = text.split('\n');
|
|
47
|
+
const wrapped = [];
|
|
48
|
+
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
if (line.length <= maxWidth) {
|
|
51
|
+
wrapped.push(line);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Word wrap logic
|
|
56
|
+
const words = line.split(' ');
|
|
57
|
+
let currentLine = '';
|
|
58
|
+
|
|
59
|
+
for (const word of words) {
|
|
60
|
+
const testLine = currentLine ? `${currentLine} ${word}` : word;
|
|
61
|
+
|
|
62
|
+
if (testLine.length <= maxWidth) {
|
|
63
|
+
currentLine = testLine;
|
|
64
|
+
} else {
|
|
65
|
+
if (currentLine) {
|
|
66
|
+
wrapped.push(currentLine);
|
|
67
|
+
}
|
|
68
|
+
// If single word is too long, force split it
|
|
69
|
+
if (word.length > maxWidth) {
|
|
70
|
+
let remaining = word;
|
|
71
|
+
while (remaining.length > maxWidth) {
|
|
72
|
+
wrapped.push(remaining.substring(0, maxWidth));
|
|
73
|
+
remaining = remaining.substring(maxWidth);
|
|
74
|
+
}
|
|
75
|
+
currentLine = remaining;
|
|
76
|
+
} else {
|
|
77
|
+
currentLine = word;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (currentLine) {
|
|
83
|
+
wrapped.push(currentLine);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return wrapped.length > 0 ? wrapped : [''];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Pads text to fit within box width
|
|
92
|
+
* @param {string} text - Text to pad
|
|
93
|
+
* @param {number} width - Target width
|
|
94
|
+
* @returns {string} Padded text
|
|
95
|
+
*/
|
|
96
|
+
function padLine(text, width) {
|
|
97
|
+
const padding = width - text.length;
|
|
98
|
+
return text + ' '.repeat(Math.max(0, padding));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Renders an error in a Next.js-style red box
|
|
103
|
+
* @param {Object} errorInfo - Error information
|
|
104
|
+
* @param {string} errorInfo.title - Error title (e.g., "Build Error")
|
|
105
|
+
* @param {string} errorInfo.file - File path where error occurred
|
|
106
|
+
* @param {string} errorInfo.message - Error message
|
|
107
|
+
* @param {string} [errorInfo.location] - Error location (e.g., "at hello.js:3:1")
|
|
108
|
+
* @param {number} [errorInfo.line] - Line number
|
|
109
|
+
* @param {number} [errorInfo.column] - Column number
|
|
110
|
+
* @param {string} [errorInfo.codeFrame] - Code frame showing error context
|
|
111
|
+
* @param {string} [errorInfo.suggestion] - Recommended fix
|
|
112
|
+
*/
|
|
113
|
+
/**
|
|
114
|
+
* Renders an error in a Next.js-style red box
|
|
115
|
+
* @param {Object} errorInfo - Error information
|
|
116
|
+
* @param {string} errorInfo.title - Error title (e.g., "Build Error")
|
|
117
|
+
* @param {string} errorInfo.file - File path where error occurred
|
|
118
|
+
* @param {string} errorInfo.message - Error message
|
|
119
|
+
* @param {string} [errorInfo.location] - Error location (e.g., "at hello.js:3:1")
|
|
120
|
+
* @param {number} [errorInfo.line] - Line number
|
|
121
|
+
* @param {number} [errorInfo.column] - Column number
|
|
122
|
+
* @param {string} [errorInfo.codeFrame] - Code frame showing error context
|
|
123
|
+
* @param {string} [errorInfo.suggestion] - Recommended fix
|
|
124
|
+
*/
|
|
125
|
+
export function renderErrorBox(errorInfo) {
|
|
126
|
+
const boxWidth = 72;
|
|
127
|
+
const contentWidth = boxWidth - 4; // Account for "│ " and " │"
|
|
128
|
+
|
|
129
|
+
const lines = [];
|
|
130
|
+
|
|
131
|
+
// Add title
|
|
132
|
+
if (errorInfo.title) {
|
|
133
|
+
lines.push(bold(errorInfo.title));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Add file path
|
|
137
|
+
if (errorInfo.file) {
|
|
138
|
+
lines.push(errorInfo.file);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Add message
|
|
142
|
+
if (errorInfo.message) {
|
|
143
|
+
lines.push('');
|
|
144
|
+
lines.push(...wrapText(errorInfo.message, contentWidth));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Add location
|
|
148
|
+
if (errorInfo.location) {
|
|
149
|
+
lines.push(gray(errorInfo.location));
|
|
150
|
+
} else if (errorInfo.file && errorInfo.line !== undefined) {
|
|
151
|
+
const loc = `at ${errorInfo.file}:${errorInfo.line}${errorInfo.column !== undefined ? `:${errorInfo.column}` : ''}`;
|
|
152
|
+
lines.push(gray(loc));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Add code frame if available
|
|
156
|
+
if (errorInfo.codeFrame) {
|
|
157
|
+
lines.push(''); // Empty line for separation
|
|
158
|
+
const frameLines = errorInfo.codeFrame.split('\n');
|
|
159
|
+
for (const frameLine of frameLines) {
|
|
160
|
+
lines.push(frameLine);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Add suggestion if available
|
|
165
|
+
if (errorInfo.suggestion) {
|
|
166
|
+
lines.push(''); // Empty line for separation
|
|
167
|
+
lines.push(...wrapText('Recommended fix: ' + errorInfo.suggestion, contentWidth));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Add Footer with Branding
|
|
171
|
+
lines.push('');
|
|
172
|
+
const version = getTitanVersion()
|
|
173
|
+
lines.push(gray(`⏣ Titan Planet ${version}`));
|
|
174
|
+
|
|
175
|
+
// Build the box
|
|
176
|
+
const topBorder = '┌' + '─'.repeat(boxWidth - 2) + '┐';
|
|
177
|
+
const bottomBorder = '└' + '─'.repeat(boxWidth - 2) + '┘';
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
const boxLines = [
|
|
181
|
+
red(topBorder),
|
|
182
|
+
...lines.map(line => {
|
|
183
|
+
// Strip ANSI codes for padding calculation if any were added
|
|
184
|
+
const plainLine = line.replace(/\x1b\[\d+m/g, '');
|
|
185
|
+
const padding = ' '.repeat(Math.max(0, contentWidth - plainLine.length));
|
|
186
|
+
return red('│ ') + line + padding + red(' │');
|
|
187
|
+
}),
|
|
188
|
+
red(bottomBorder)
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
return boxLines.join('\n');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Internal formatting helpers
|
|
195
|
+
function gray(t) { return `\x1b[90m${t}\x1b[0m`; }
|
|
196
|
+
function bold(t) { return `\x1b[1m${t}\x1b[0m`; }
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Parses esbuild error and extracts relevant information
|
|
200
|
+
* @param {Object} error - esbuild error object
|
|
201
|
+
* @returns {Object} Parsed error information
|
|
202
|
+
*/
|
|
203
|
+
export function parseEsbuildError(error) {
|
|
204
|
+
const errorInfo = {
|
|
205
|
+
title: 'Build Error',
|
|
206
|
+
file: error.location?.file || 'unknown',
|
|
207
|
+
message: error.text || error.message || 'Unknown error',
|
|
208
|
+
line: error.location?.line,
|
|
209
|
+
column: error.location?.column,
|
|
210
|
+
location: null,
|
|
211
|
+
codeFrame: null,
|
|
212
|
+
suggestion: error.notes?.[0]?.text || null
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// Format location
|
|
216
|
+
if (error.location) {
|
|
217
|
+
const { file, line, column } = error.location;
|
|
218
|
+
errorInfo.location = `at ${file}:${line}:${column}`;
|
|
219
|
+
|
|
220
|
+
// Format code frame if lineText is available
|
|
221
|
+
if (error.location.lineText) {
|
|
222
|
+
const lineText = error.location.lineText;
|
|
223
|
+
// Ensure column is at least 1 to prevent negative values
|
|
224
|
+
const col = Math.max(0, (column || 1) - 1);
|
|
225
|
+
const pointer = ' '.repeat(col) + '^';
|
|
226
|
+
errorInfo.codeFrame = `${line} | ${lineText}\n${' '.repeat(String(line).length)} | ${pointer}`;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return errorInfo;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Parses Node.js syntax error and extracts relevant information
|
|
235
|
+
* @param {Error} error - Node.js error object
|
|
236
|
+
* @param {string} [file] - File path (if known)
|
|
237
|
+
* @returns {Object} Parsed error information
|
|
238
|
+
*/
|
|
239
|
+
export function parseNodeError(error, file = null) {
|
|
240
|
+
const errorInfo = {
|
|
241
|
+
title: 'Syntax Error',
|
|
242
|
+
file: file || 'unknown',
|
|
243
|
+
message: error.message || 'Unknown error',
|
|
244
|
+
location: null,
|
|
245
|
+
suggestion: null
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// Try to extract line and column from error message
|
|
249
|
+
const locationMatch = error.message.match(/\((\d+):(\d+)\)/) ||
|
|
250
|
+
error.stack?.match(/:(\d+):(\d+)/);
|
|
251
|
+
|
|
252
|
+
if (locationMatch) {
|
|
253
|
+
const line = parseInt(locationMatch[1]);
|
|
254
|
+
const column = parseInt(locationMatch[2]);
|
|
255
|
+
errorInfo.line = line;
|
|
256
|
+
errorInfo.column = column;
|
|
257
|
+
errorInfo.location = `at ${errorInfo.file}:${line}:${column}`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Extract suggestion from error message if available
|
|
261
|
+
if (error.message.includes('expected')) {
|
|
262
|
+
errorInfo.suggestion = 'Check for missing or misplaced syntax elements';
|
|
263
|
+
} else if (error.message.includes('Unexpected token')) {
|
|
264
|
+
errorInfo.suggestion = 'Remove or fix the unexpected token';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return errorInfo;
|
|
268
|
+
}
|