@buenojs/bueno 0.8.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/.env.example +109 -0
- package/.github/workflows/ci.yml +31 -0
- package/LICENSE +21 -0
- package/README.md +892 -0
- package/architecture.md +652 -0
- package/bun.lock +70 -0
- package/dist/cli/index.js +3233 -0
- package/dist/index.js +9014 -0
- package/package.json +77 -0
- package/src/cache/index.ts +795 -0
- package/src/cli/ARCHITECTURE.md +837 -0
- package/src/cli/bin.ts +10 -0
- package/src/cli/commands/build.ts +425 -0
- package/src/cli/commands/dev.ts +248 -0
- package/src/cli/commands/generate.ts +541 -0
- package/src/cli/commands/help.ts +55 -0
- package/src/cli/commands/index.ts +112 -0
- package/src/cli/commands/migration.ts +355 -0
- package/src/cli/commands/new.ts +804 -0
- package/src/cli/commands/start.ts +208 -0
- package/src/cli/core/args.ts +283 -0
- package/src/cli/core/console.ts +349 -0
- package/src/cli/core/index.ts +60 -0
- package/src/cli/core/prompt.ts +424 -0
- package/src/cli/core/spinner.ts +265 -0
- package/src/cli/index.ts +135 -0
- package/src/cli/templates/deploy.ts +295 -0
- package/src/cli/templates/docker.ts +307 -0
- package/src/cli/templates/index.ts +24 -0
- package/src/cli/utils/fs.ts +428 -0
- package/src/cli/utils/index.ts +8 -0
- package/src/cli/utils/strings.ts +197 -0
- package/src/config/env.ts +408 -0
- package/src/config/index.ts +506 -0
- package/src/config/loader.ts +329 -0
- package/src/config/merge.ts +285 -0
- package/src/config/types.ts +320 -0
- package/src/config/validation.ts +441 -0
- package/src/container/forward-ref.ts +143 -0
- package/src/container/index.ts +386 -0
- package/src/context/index.ts +360 -0
- package/src/database/index.ts +1142 -0
- package/src/database/migrations/index.ts +371 -0
- package/src/database/schema/index.ts +619 -0
- package/src/frontend/api-routes.ts +640 -0
- package/src/frontend/bundler.ts +643 -0
- package/src/frontend/console-client.ts +419 -0
- package/src/frontend/console-stream.ts +587 -0
- package/src/frontend/dev-server.ts +846 -0
- package/src/frontend/file-router.ts +611 -0
- package/src/frontend/frameworks/index.ts +106 -0
- package/src/frontend/frameworks/react.ts +85 -0
- package/src/frontend/frameworks/solid.ts +104 -0
- package/src/frontend/frameworks/svelte.ts +110 -0
- package/src/frontend/frameworks/vue.ts +92 -0
- package/src/frontend/hmr-client.ts +663 -0
- package/src/frontend/hmr.ts +728 -0
- package/src/frontend/index.ts +342 -0
- package/src/frontend/islands.ts +552 -0
- package/src/frontend/isr.ts +555 -0
- package/src/frontend/layout.ts +475 -0
- package/src/frontend/ssr/react.ts +446 -0
- package/src/frontend/ssr/solid.ts +523 -0
- package/src/frontend/ssr/svelte.ts +546 -0
- package/src/frontend/ssr/vue.ts +504 -0
- package/src/frontend/ssr.ts +699 -0
- package/src/frontend/types.ts +2274 -0
- package/src/health/index.ts +604 -0
- package/src/index.ts +410 -0
- package/src/lock/index.ts +587 -0
- package/src/logger/index.ts +444 -0
- package/src/logger/transports/index.ts +969 -0
- package/src/metrics/index.ts +494 -0
- package/src/middleware/built-in.ts +360 -0
- package/src/middleware/index.ts +94 -0
- package/src/modules/filters.ts +458 -0
- package/src/modules/guards.ts +405 -0
- package/src/modules/index.ts +1256 -0
- package/src/modules/interceptors.ts +574 -0
- package/src/modules/lazy.ts +418 -0
- package/src/modules/lifecycle.ts +478 -0
- package/src/modules/metadata.ts +90 -0
- package/src/modules/pipes.ts +626 -0
- package/src/router/index.ts +339 -0
- package/src/router/linear.ts +371 -0
- package/src/router/regex.ts +292 -0
- package/src/router/tree.ts +562 -0
- package/src/rpc/index.ts +1263 -0
- package/src/security/index.ts +436 -0
- package/src/ssg/index.ts +631 -0
- package/src/storage/index.ts +456 -0
- package/src/telemetry/index.ts +1097 -0
- package/src/testing/index.ts +1586 -0
- package/src/types/index.ts +236 -0
- package/src/types/optional-deps.d.ts +219 -0
- package/src/validation/index.ts +276 -0
- package/src/websocket/index.ts +1004 -0
- package/tests/integration/cli.test.ts +1016 -0
- package/tests/integration/fullstack.test.ts +234 -0
- package/tests/unit/cache.test.ts +174 -0
- package/tests/unit/cli-commands.test.ts +892 -0
- package/tests/unit/cli.test.ts +1258 -0
- package/tests/unit/container.test.ts +279 -0
- package/tests/unit/context.test.ts +221 -0
- package/tests/unit/database.test.ts +183 -0
- package/tests/unit/linear-router.test.ts +280 -0
- package/tests/unit/lock.test.ts +336 -0
- package/tests/unit/middleware.test.ts +184 -0
- package/tests/unit/modules.test.ts +142 -0
- package/tests/unit/pubsub.test.ts +257 -0
- package/tests/unit/regex-router.test.ts +265 -0
- package/tests/unit/router.test.ts +373 -0
- package/tests/unit/rpc.test.ts +1248 -0
- package/tests/unit/security.test.ts +174 -0
- package/tests/unit/telemetry.test.ts +371 -0
- package/tests/unit/test-cache.test.ts +110 -0
- package/tests/unit/test-database.test.ts +282 -0
- package/tests/unit/tree-router.test.ts +325 -0
- package/tests/unit/validation.test.ts +794 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser Console Streaming Implementation
|
|
3
|
+
*
|
|
4
|
+
* Captures console.* calls from the browser and streams them to the terminal,
|
|
5
|
+
* providing a unified debugging experience.
|
|
6
|
+
*
|
|
7
|
+
* @module frontend/console-stream
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createLogger, type Logger } from "../logger/index.js";
|
|
11
|
+
import type {
|
|
12
|
+
ConsoleMessage,
|
|
13
|
+
ConsoleStreamConfig,
|
|
14
|
+
ConsoleStreamClient,
|
|
15
|
+
ConsoleClientMessage,
|
|
16
|
+
ConsoleServerMessage,
|
|
17
|
+
ConsoleMessageType,
|
|
18
|
+
PartialConsoleStreamConfig,
|
|
19
|
+
} from "./types.js";
|
|
20
|
+
import { CONSOLE_CLIENT_SCRIPT } from "./console-client.js";
|
|
21
|
+
|
|
22
|
+
// ============= Constants =============
|
|
23
|
+
|
|
24
|
+
const DEFAULT_PORT_OFFSET = 2; // Console stream port = dev server port + 2
|
|
25
|
+
|
|
26
|
+
// ANSI color codes for terminal output
|
|
27
|
+
const ANSI_COLORS = {
|
|
28
|
+
reset: "\x1b[0m",
|
|
29
|
+
bold: "\x1b[1m",
|
|
30
|
+
dim: "\x1b[2m",
|
|
31
|
+
white: "\x1b[37m",
|
|
32
|
+
gray: "\x1b[90m",
|
|
33
|
+
cyan: "\x1b[36m",
|
|
34
|
+
green: "\x1b[32m",
|
|
35
|
+
yellow: "\x1b[33m",
|
|
36
|
+
red: "\x1b[31m",
|
|
37
|
+
magenta: "\x1b[35m",
|
|
38
|
+
blue: "\x1b[34m",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Console type colors
|
|
42
|
+
const CONSOLE_TYPE_COLORS: Record<ConsoleMessageType, string> = {
|
|
43
|
+
log: ANSI_COLORS.white,
|
|
44
|
+
info: ANSI_COLORS.cyan,
|
|
45
|
+
warn: ANSI_COLORS.yellow,
|
|
46
|
+
error: ANSI_COLORS.red,
|
|
47
|
+
debug: ANSI_COLORS.gray,
|
|
48
|
+
trace: ANSI_COLORS.magenta,
|
|
49
|
+
table: ANSI_COLORS.blue,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// ============= ConsoleStreamManager Class =============
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Manages browser console streaming to the terminal.
|
|
56
|
+
*
|
|
57
|
+
* Features:
|
|
58
|
+
* - WebSocket server for receiving console messages from browser
|
|
59
|
+
* - Color-coded terminal output
|
|
60
|
+
* - File:line clickable links for VSCode
|
|
61
|
+
* - Object/array pretty printing
|
|
62
|
+
* - Stack traces for errors
|
|
63
|
+
* - Source map support for original file references
|
|
64
|
+
*/
|
|
65
|
+
export class ConsoleStreamManager {
|
|
66
|
+
private config: ConsoleStreamConfig;
|
|
67
|
+
private logger: Logger;
|
|
68
|
+
private clients: Map<string, ConsoleStreamClient> = new Map();
|
|
69
|
+
private devServerPort: number;
|
|
70
|
+
private port: number;
|
|
71
|
+
private server: ReturnType<typeof Bun.serve> | null = null;
|
|
72
|
+
|
|
73
|
+
constructor(devServerPort: number, config?: PartialConsoleStreamConfig) {
|
|
74
|
+
this.devServerPort = devServerPort;
|
|
75
|
+
this.port = devServerPort + DEFAULT_PORT_OFFSET;
|
|
76
|
+
this.config = this.normalizeConfig(config);
|
|
77
|
+
this.logger = createLogger({
|
|
78
|
+
level: "debug",
|
|
79
|
+
pretty: true,
|
|
80
|
+
context: { component: "ConsoleStream" },
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Normalize partial config to full config with defaults
|
|
86
|
+
*/
|
|
87
|
+
private normalizeConfig(config?: PartialConsoleStreamConfig): ConsoleStreamConfig {
|
|
88
|
+
return {
|
|
89
|
+
enabled: config?.enabled ?? true,
|
|
90
|
+
showTimestamps: config?.showTimestamps ?? true,
|
|
91
|
+
showFile: config?.showFile ?? true,
|
|
92
|
+
colorize: config?.colorize ?? true,
|
|
93
|
+
filter: config?.filter ?? ['log', 'info', 'warn', 'error', 'debug', 'trace', 'table'],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get the console client script for injection
|
|
99
|
+
*/
|
|
100
|
+
getClientScript(): string {
|
|
101
|
+
return CONSOLE_CLIENT_SCRIPT;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get the WebSocket URL for console streaming
|
|
106
|
+
*/
|
|
107
|
+
getWebSocketUrl(): string {
|
|
108
|
+
return `ws://localhost:${this.port}/_console`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get the console stream port
|
|
113
|
+
*/
|
|
114
|
+
getPort(): number {
|
|
115
|
+
return this.port;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Check if console streaming is enabled
|
|
120
|
+
*/
|
|
121
|
+
isEnabled(): boolean {
|
|
122
|
+
return this.config.enabled;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Start the console stream WebSocket server
|
|
127
|
+
*/
|
|
128
|
+
start(): void {
|
|
129
|
+
if (!this.config.enabled) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (this.server) {
|
|
134
|
+
this.logger.warn("Console stream server already running");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
this.server = Bun.serve({
|
|
139
|
+
port: this.port,
|
|
140
|
+
fetch: this.handleFetch.bind(this),
|
|
141
|
+
websocket: {
|
|
142
|
+
open: this.handleOpen.bind(this),
|
|
143
|
+
close: this.handleClose.bind(this),
|
|
144
|
+
message: this.handleMessage.bind(this),
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
this.logger.info(`Console stream server started on port ${this.port}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Stop the console stream server
|
|
153
|
+
*/
|
|
154
|
+
stop(): void {
|
|
155
|
+
if (this.server) {
|
|
156
|
+
// Disconnect all clients
|
|
157
|
+
for (const client of this.clients.values()) {
|
|
158
|
+
if (client.ws.readyState === WebSocket.OPEN) {
|
|
159
|
+
client.ws.close();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
this.clients.clear();
|
|
163
|
+
|
|
164
|
+
this.server.stop();
|
|
165
|
+
this.server = null;
|
|
166
|
+
this.logger.info("Console stream server stopped");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Handle fetch requests for WebSocket server
|
|
172
|
+
*/
|
|
173
|
+
private handleFetch(request: Request, server: any): Response | undefined {
|
|
174
|
+
const url = new URL(request.url);
|
|
175
|
+
|
|
176
|
+
if (url.pathname === "/_console") {
|
|
177
|
+
const upgradeHeader = request.headers.get("upgrade");
|
|
178
|
+
if (upgradeHeader !== "websocket") {
|
|
179
|
+
return new Response("Expected WebSocket upgrade", { status: 426 });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const success = server.upgrade(request);
|
|
183
|
+
if (success) {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
return new Response("WebSocket upgrade failed", { status: 400 });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return new Response("Not found", { status: 404 });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Handle WebSocket connection open
|
|
194
|
+
*/
|
|
195
|
+
private handleOpen(ws: any): void {
|
|
196
|
+
const clientId = this.generateClientId();
|
|
197
|
+
const client: ConsoleStreamClient = {
|
|
198
|
+
id: clientId,
|
|
199
|
+
ws: ws,
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
this.clients.set(clientId, client);
|
|
203
|
+
ws.data = { clientId };
|
|
204
|
+
|
|
205
|
+
this.logger.debug(`Console client connected: ${clientId}`);
|
|
206
|
+
|
|
207
|
+
// Send connected message
|
|
208
|
+
this.sendToClient(ws, {
|
|
209
|
+
type: "connected",
|
|
210
|
+
clientId,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Handle WebSocket connection close
|
|
216
|
+
*/
|
|
217
|
+
private handleClose(ws: any): void {
|
|
218
|
+
const clientId = ws.data?.clientId;
|
|
219
|
+
if (clientId) {
|
|
220
|
+
this.clients.delete(clientId);
|
|
221
|
+
this.logger.debug(`Console client disconnected: ${clientId}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Handle WebSocket message from client
|
|
227
|
+
*/
|
|
228
|
+
private handleMessage(ws: any, message: string | Buffer): void {
|
|
229
|
+
try {
|
|
230
|
+
const data: ConsoleClientMessage = JSON.parse(message.toString());
|
|
231
|
+
|
|
232
|
+
if (data.type === "console") {
|
|
233
|
+
// Update client URL if provided
|
|
234
|
+
const clientId = ws.data?.clientId;
|
|
235
|
+
if (clientId && data.url) {
|
|
236
|
+
const client = this.clients.get(clientId);
|
|
237
|
+
if (client) {
|
|
238
|
+
client.url = data.url;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Process and display the console message
|
|
243
|
+
this.processConsoleMessage(data);
|
|
244
|
+
}
|
|
245
|
+
} catch (error) {
|
|
246
|
+
this.logger.error("Failed to parse console message", error);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Send message to a WebSocket client
|
|
252
|
+
*/
|
|
253
|
+
private sendToClient(ws: WebSocket, message: ConsoleServerMessage): void {
|
|
254
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
255
|
+
ws.send(JSON.stringify(message));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Process and display a console message
|
|
261
|
+
*/
|
|
262
|
+
private processConsoleMessage(message: ConsoleClientMessage): void {
|
|
263
|
+
// Check if this message type should be filtered
|
|
264
|
+
if (!this.config.filter.includes(message.consoleType)) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Format and output the message
|
|
269
|
+
const formatted = this.formatMessage(message);
|
|
270
|
+
this.output(formatted, message.consoleType);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Format a console message for terminal output
|
|
275
|
+
*/
|
|
276
|
+
private formatMessage(message: ConsoleClientMessage): string {
|
|
277
|
+
const parts: string[] = [];
|
|
278
|
+
|
|
279
|
+
// Timestamp
|
|
280
|
+
if (this.config.showTimestamps) {
|
|
281
|
+
const timestamp = this.formatTimestamp(message.timestamp);
|
|
282
|
+
if (this.config.colorize) {
|
|
283
|
+
parts.push(`${ANSI_COLORS.dim}[${timestamp}]${ANSI_COLORS.reset}`);
|
|
284
|
+
} else {
|
|
285
|
+
parts.push(`[${timestamp}]`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Message type with color
|
|
290
|
+
const typeColor = this.config.colorize ? CONSOLE_TYPE_COLORS[message.consoleType] : "";
|
|
291
|
+
const typeReset = this.config.colorize ? ANSI_COLORS.reset : "";
|
|
292
|
+
const typeLabel = message.consoleType.toUpperCase().padEnd(5);
|
|
293
|
+
parts.push(`${typeColor}${typeLabel}${typeReset}`);
|
|
294
|
+
|
|
295
|
+
// Message arguments
|
|
296
|
+
const formattedArgs = this.formatArgs(message.args, message.consoleType);
|
|
297
|
+
parts.push(formattedArgs);
|
|
298
|
+
|
|
299
|
+
// File:line information
|
|
300
|
+
if (this.config.showFile && message.file) {
|
|
301
|
+
const fileLink = this.formatFileLink(message.file, message.line, message.column);
|
|
302
|
+
parts.push(`\n at ${fileLink}`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Stack trace for errors
|
|
306
|
+
if (message.stack) {
|
|
307
|
+
const formattedStack = this.formatStackTrace(message.stack);
|
|
308
|
+
parts.push(`\n${formattedStack}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return parts.join(" ");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Format timestamp for display
|
|
316
|
+
*/
|
|
317
|
+
private formatTimestamp(timestamp: number): string {
|
|
318
|
+
const date = new Date(timestamp);
|
|
319
|
+
const hours = date.getHours().toString().padStart(2, "0");
|
|
320
|
+
const minutes = date.getMinutes().toString().padStart(2, "0");
|
|
321
|
+
const seconds = date.getSeconds().toString().padStart(2, "0");
|
|
322
|
+
const ms = date.getMilliseconds().toString().padStart(3, "0");
|
|
323
|
+
return `${hours}:${minutes}:${seconds}.${ms}`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Format console arguments for display
|
|
328
|
+
*/
|
|
329
|
+
private formatArgs(args: unknown[], type: ConsoleMessageType): string {
|
|
330
|
+
if (type === "table") {
|
|
331
|
+
return this.formatTable(args);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (type === "trace") {
|
|
335
|
+
// Trace already includes the stack in the args
|
|
336
|
+
return args.map(arg => this.formatValue(arg)).join(" ");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return args.map(arg => this.formatValue(arg)).join(" ");
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Format a single value for display
|
|
344
|
+
*/
|
|
345
|
+
private formatValue(value: unknown, depth: number = 0): string {
|
|
346
|
+
if (depth > 3) {
|
|
347
|
+
return this.config.colorize ? `${ANSI_COLORS.dim}[...]${ANSI_COLORS.reset}` : "[...]";
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (value === null) {
|
|
351
|
+
return this.config.colorize ? `${ANSI_COLORS.gray}null${ANSI_COLORS.reset}` : "null";
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (value === undefined) {
|
|
355
|
+
return this.config.colorize ? `${ANSI_COLORS.gray}undefined${ANSI_COLORS.reset}` : "undefined";
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (typeof value === "string") {
|
|
359
|
+
// Check if it's a long string
|
|
360
|
+
if (value.length > 200) {
|
|
361
|
+
const truncated = value.substring(0, 200) + "...";
|
|
362
|
+
return this.config.colorize ? `${ANSI_COLORS.green}"${truncated}"${ANSI_COLORS.reset}` : `"${truncated}"`;
|
|
363
|
+
}
|
|
364
|
+
return this.config.colorize ? `${ANSI_COLORS.green}"${value}"${ANSI_COLORS.reset}` : `"${value}"`;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (typeof value === "number") {
|
|
368
|
+
return this.config.colorize ? `${ANSI_COLORS.yellow}${value}${ANSI_COLORS.reset}` : `${value}`;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (typeof value === "boolean") {
|
|
372
|
+
return this.config.colorize ? `${ANSI_COLORS.magenta}${value}${ANSI_COLORS.reset}` : `${value}`;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (value instanceof Error) {
|
|
376
|
+
const errorStr = `${value.name}: ${value.message}`;
|
|
377
|
+
return this.config.colorize ? `${ANSI_COLORS.red}${errorStr}${ANSI_COLORS.reset}` : errorStr;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (Array.isArray(value)) {
|
|
381
|
+
if (value.length === 0) {
|
|
382
|
+
return "[]";
|
|
383
|
+
}
|
|
384
|
+
if (value.length > 10) {
|
|
385
|
+
const items = value.slice(0, 10).map(v => this.formatValue(v, depth + 1));
|
|
386
|
+
return `[${items.join(", ")}, ... ${value.length - 10} more items]`;
|
|
387
|
+
}
|
|
388
|
+
const items = value.map(v => this.formatValue(v, depth + 1));
|
|
389
|
+
return `[${items.join(", ")}]`;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (typeof value === "object") {
|
|
393
|
+
try {
|
|
394
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
395
|
+
if (entries.length === 0) {
|
|
396
|
+
return "{}";
|
|
397
|
+
}
|
|
398
|
+
if (entries.length > 5) {
|
|
399
|
+
const shown = entries.slice(0, 5).map(([k, v]) => `${k}: ${this.formatValue(v, depth + 1)}`);
|
|
400
|
+
return `{${shown.join(", ")}, ... ${entries.length - 5} more keys}`;
|
|
401
|
+
}
|
|
402
|
+
const formatted = entries.map(([k, v]) => `${k}: ${this.formatValue(v, depth + 1)}`);
|
|
403
|
+
return `{${formatted.join(", ")}}`;
|
|
404
|
+
} catch {
|
|
405
|
+
return "[Object]";
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return String(value);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Format console.table output
|
|
414
|
+
*/
|
|
415
|
+
private formatTable(args: unknown[]): string {
|
|
416
|
+
if (args.length === 0) return "";
|
|
417
|
+
|
|
418
|
+
const [data, columns] = args;
|
|
419
|
+
|
|
420
|
+
if (!Array.isArray(data) && typeof data !== "object") {
|
|
421
|
+
return this.formatValue(data);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Simple table formatting
|
|
425
|
+
const entries = Array.isArray(data) ? data : Object.entries(data as object);
|
|
426
|
+
|
|
427
|
+
if (entries.length === 0) {
|
|
428
|
+
return this.config.colorize ? `${ANSI_COLORS.dim}(empty table)${ANSI_COLORS.reset}` : "(empty table)";
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const lines: string[] = [];
|
|
432
|
+
lines.push(this.config.colorize ? `${ANSI_COLORS.blue}┌─────────${ANSI_COLORS.reset}` : "┌─────────");
|
|
433
|
+
|
|
434
|
+
const maxRows = 10;
|
|
435
|
+
const shown = entries.slice(0, maxRows);
|
|
436
|
+
|
|
437
|
+
for (const entry of shown) {
|
|
438
|
+
const row = this.formatValue(entry, 1);
|
|
439
|
+
lines.push(this.config.colorize ? `${ANSI_COLORS.blue}│${ANSI_COLORS.reset} ${row}` : `│ ${row}`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (entries.length > maxRows) {
|
|
443
|
+
lines.push(this.config.colorize ? `${ANSI_COLORS.blue}│${ANSI_COLORS.reset} ... ${entries.length - maxRows} more rows` : `│ ... ${entries.length - maxRows} more rows`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
lines.push(this.config.colorize ? `${ANSI_COLORS.blue}└─────────${ANSI_COLORS.reset}` : "└─────────");
|
|
447
|
+
|
|
448
|
+
return "\n" + lines.join("\n");
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Format file link for VSCode clickable links
|
|
453
|
+
*/
|
|
454
|
+
private formatFileLink(file: string, line?: number, column?: number): string {
|
|
455
|
+
const location = line ? `:${line}${column ? `:${column}` : ""}` : "";
|
|
456
|
+
const link = `${file}${location}`;
|
|
457
|
+
|
|
458
|
+
if (this.config.colorize) {
|
|
459
|
+
return `${ANSI_COLORS.cyan}${link}${ANSI_COLORS.reset}`;
|
|
460
|
+
}
|
|
461
|
+
return link;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Format stack trace for display
|
|
466
|
+
*/
|
|
467
|
+
private formatStackTrace(stack: string): string {
|
|
468
|
+
const lines = stack.split("\n");
|
|
469
|
+
const formatted = lines.map((line, index) => {
|
|
470
|
+
if (index === 0) {
|
|
471
|
+
// First line is usually the error message
|
|
472
|
+
return this.config.colorize
|
|
473
|
+
? ` ${ANSI_COLORS.red}${line}${ANSI_COLORS.reset}`
|
|
474
|
+
: ` ${line}`;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Try to make file paths clickable
|
|
478
|
+
const match = line.match(/at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/);
|
|
479
|
+
if (match) {
|
|
480
|
+
const [, fn, file, lineNum, col] = match;
|
|
481
|
+
if (this.config.colorize) {
|
|
482
|
+
return ` at ${fn} (${ANSI_COLORS.cyan}${file}:${lineNum}:${col}${ANSI_COLORS.reset})`;
|
|
483
|
+
}
|
|
484
|
+
return ` at ${fn} (${file}:${lineNum}:${col})`;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return this.config.colorize
|
|
488
|
+
? ` ${ANSI_COLORS.gray}${line}${ANSI_COLORS.reset}`
|
|
489
|
+
: ` ${line}`;
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
return formatted.join("\n");
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Output formatted message to terminal
|
|
497
|
+
*/
|
|
498
|
+
private output(formatted: string, type: ConsoleMessageType): void {
|
|
499
|
+
switch (type) {
|
|
500
|
+
case "error":
|
|
501
|
+
console.error(formatted);
|
|
502
|
+
break;
|
|
503
|
+
case "warn":
|
|
504
|
+
console.warn(formatted);
|
|
505
|
+
break;
|
|
506
|
+
default:
|
|
507
|
+
console.log(formatted);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Generate a unique client ID
|
|
513
|
+
*/
|
|
514
|
+
private generateClientId(): string {
|
|
515
|
+
return `console_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Get the number of connected clients
|
|
520
|
+
*/
|
|
521
|
+
getClientCount(): number {
|
|
522
|
+
return this.clients.size;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Get all connected client IDs
|
|
527
|
+
*/
|
|
528
|
+
getClientIds(): string[] {
|
|
529
|
+
return Array.from(this.clients.keys());
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Disconnect all clients
|
|
534
|
+
*/
|
|
535
|
+
disconnectAll(): void {
|
|
536
|
+
for (const client of this.clients.values()) {
|
|
537
|
+
if (client.ws.readyState === WebSocket.OPEN) {
|
|
538
|
+
client.ws.close();
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
this.clients.clear();
|
|
543
|
+
this.logger.info("All console clients disconnected");
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// ============= Factory Function =============
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Create a console stream manager
|
|
551
|
+
*/
|
|
552
|
+
export function createConsoleStreamManager(
|
|
553
|
+
devServerPort: number,
|
|
554
|
+
config?: PartialConsoleStreamConfig
|
|
555
|
+
): ConsoleStreamManager {
|
|
556
|
+
return new ConsoleStreamManager(devServerPort, config);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// ============= Utility Functions =============
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Inject console client script into HTML
|
|
563
|
+
*/
|
|
564
|
+
export function injectConsoleScript(html: string, port: number): string {
|
|
565
|
+
const script = `
|
|
566
|
+
<script>
|
|
567
|
+
(function() {
|
|
568
|
+
const CONSOLE_PORT = ${port};
|
|
569
|
+
${CONSOLE_CLIENT_SCRIPT}
|
|
570
|
+
})();
|
|
571
|
+
</script>
|
|
572
|
+
`;
|
|
573
|
+
|
|
574
|
+
// Inject before closing </head> or <body>
|
|
575
|
+
const headMatch = html.match(/<\/head>/i);
|
|
576
|
+
if (headMatch) {
|
|
577
|
+
return html.replace(/<\/head>/i, `${script}</head>`);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const bodyMatch = html.match(/<body/i);
|
|
581
|
+
if (bodyMatch) {
|
|
582
|
+
return html.replace(/<body/i, `${script}<body`);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// If no head or body, prepend
|
|
586
|
+
return script + html;
|
|
587
|
+
}
|