@leftium/gg 0.0.26 → 0.0.28
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 +61 -1
- package/dist/eruda/loader.js +14 -1
- package/dist/eruda/plugin.js +297 -70
- package/dist/gg-tag-plugin.d.ts +31 -0
- package/dist/gg-tag-plugin.js +234 -0
- package/dist/gg.d.ts +68 -1
- package/dist/gg.js +252 -23
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,7 +20,67 @@ npm add @leftium/gg
|
|
|
20
20
|
|
|
21
21
|
## Usage
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
### Basic Logging
|
|
24
|
+
|
|
25
|
+
```javascript
|
|
26
|
+
import { gg } from '@leftium/gg';
|
|
27
|
+
|
|
28
|
+
// Simple logging
|
|
29
|
+
gg('Hello world');
|
|
30
|
+
|
|
31
|
+
// Log expressions (returns first argument)
|
|
32
|
+
const result = gg(someFunction());
|
|
33
|
+
|
|
34
|
+
// Multiple arguments
|
|
35
|
+
gg('User:', user, 'Status:', status);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Color Support (ANSI)
|
|
39
|
+
|
|
40
|
+
Color your logs for better visual distinction using `fg()` (foreground/text) and `bg()` (background):
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
import { gg, fg, bg } from '@leftium/gg';
|
|
44
|
+
|
|
45
|
+
// Simple foreground/background colors
|
|
46
|
+
gg(fg('red')`Error occurred`);
|
|
47
|
+
gg(bg('yellow')`Warning message`);
|
|
48
|
+
|
|
49
|
+
// Method chaining (order doesn't matter!)
|
|
50
|
+
gg(fg('white').bg('red')`Critical error!`);
|
|
51
|
+
gg(bg('green').fg('white')`Success message`);
|
|
52
|
+
|
|
53
|
+
// Define reusable color schemes
|
|
54
|
+
const input = fg('blue').bg('yellow');
|
|
55
|
+
const transcript = bg('green').fg('white');
|
|
56
|
+
const error = fg('white').bg('red');
|
|
57
|
+
|
|
58
|
+
gg(input`User input message`);
|
|
59
|
+
gg(transcript`AI transcript response`);
|
|
60
|
+
gg(error`Something went wrong`);
|
|
61
|
+
|
|
62
|
+
// Mix colored and normal text
|
|
63
|
+
gg(fg('red')`Error: ` + bg('yellow')`warning` + ' normal text');
|
|
64
|
+
|
|
65
|
+
// Custom hex colors with chaining
|
|
66
|
+
gg(fg('#ff6347').bg('#98fb98')`Custom colors`);
|
|
67
|
+
|
|
68
|
+
// RGB colors
|
|
69
|
+
gg(fg('rgb(255,99,71)')`Tomato text`);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Supported color formats:**
|
|
73
|
+
|
|
74
|
+
- Named colors: `'red'`, `'green'`, `'blue'`, `'cyan'`, `'magenta'`, `'yellow'`, `'white'`, `'black'`, `'gray'`, `'orange'`, `'purple'`, `'pink'`
|
|
75
|
+
- Hex codes: `'#ff0000'`, `'#f00'`
|
|
76
|
+
- RGB: `'rgb(255,0,0)'`, `'rgba(255,0,0,0.5)'`
|
|
77
|
+
|
|
78
|
+
**Where colors work:**
|
|
79
|
+
|
|
80
|
+
- ✅ Native browser console (Chrome DevTools, Firefox, etc.)
|
|
81
|
+
- ✅ Eruda GG panel (mobile debugging)
|
|
82
|
+
- ✅ Node.js terminal
|
|
83
|
+
- ✅ All environments that support ANSI escape codes
|
|
24
84
|
|
|
25
85
|
## Technical Details
|
|
26
86
|
|
package/dist/eruda/loader.js
CHANGED
|
@@ -62,6 +62,17 @@ export async function loadEruda(options) {
|
|
|
62
62
|
// Dynamic import of Eruda
|
|
63
63
|
const erudaModule = await import('eruda');
|
|
64
64
|
const eruda = erudaModule.default;
|
|
65
|
+
// Clear Eruda position state to prevent icon from being stuck in wrong position
|
|
66
|
+
// Eruda stores draggable icon position in localStorage which can get corrupted
|
|
67
|
+
// This ensures the icon always appears in the default bottom-right corner
|
|
68
|
+
try {
|
|
69
|
+
// Eruda uses keys like 'eruda-entry-button' for position state
|
|
70
|
+
const positionKeys = ['eruda-entry-button', 'eruda-position'];
|
|
71
|
+
positionKeys.forEach((key) => localStorage.removeItem(key));
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// localStorage might not be available
|
|
75
|
+
}
|
|
65
76
|
// Initialize Eruda
|
|
66
77
|
eruda.init({
|
|
67
78
|
...options.erudaOptions,
|
|
@@ -81,13 +92,15 @@ export async function loadEruda(options) {
|
|
|
81
92
|
}
|
|
82
93
|
// Register gg plugin
|
|
83
94
|
// Import gg and pass it to the plugin directly
|
|
84
|
-
const { gg } = await import('../gg.js');
|
|
95
|
+
const { gg, runGgDiagnostics } = await import('../gg.js');
|
|
85
96
|
const { createGgPlugin } = await import('./plugin.js');
|
|
86
97
|
const ggPlugin = createGgPlugin(options, gg);
|
|
87
98
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
88
99
|
eruda.add(ggPlugin);
|
|
89
100
|
// Make GG tab the default selected tab
|
|
90
101
|
eruda.show('GG');
|
|
102
|
+
// Run diagnostics after Eruda is ready so they appear in Console tab
|
|
103
|
+
await runGgDiagnostics();
|
|
91
104
|
}
|
|
92
105
|
catch (error) {
|
|
93
106
|
console.error('[gg] Failed to load Eruda:', error);
|
package/dist/eruda/plugin.js
CHANGED
|
@@ -172,29 +172,37 @@ export function createGgPlugin(options, gg) {
|
|
|
172
172
|
}
|
|
173
173
|
function gridColumns() {
|
|
174
174
|
const ns = nsColWidth !== null ? `${nsColWidth}px` : 'auto';
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
return `24px auto ${ns} 4px 1fr`;
|
|
178
|
-
}
|
|
179
|
-
else {
|
|
180
|
-
// diff | ns | handle | content
|
|
181
|
-
return `auto ${ns} 4px 1fr`;
|
|
182
|
-
}
|
|
175
|
+
// diff | ns | handle | content (× is now inside ns)
|
|
176
|
+
return `auto ${ns} 4px 1fr`;
|
|
183
177
|
}
|
|
184
178
|
function buildHTML() {
|
|
185
179
|
return `
|
|
186
180
|
<style>
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
181
|
+
.gg-log-grid {
|
|
182
|
+
display: grid;
|
|
183
|
+
grid-template-columns: ${gridColumns()};
|
|
184
|
+
column-gap: 0;
|
|
185
|
+
align-items: start !important;
|
|
186
|
+
}
|
|
187
|
+
/* Desktop: hide wrapper divs, show direct children */
|
|
188
|
+
.gg-log-entry {
|
|
189
|
+
display: contents;
|
|
190
|
+
}
|
|
191
|
+
.gg-log-header {
|
|
192
|
+
display: contents;
|
|
193
|
+
}
|
|
194
|
+
.gg-log-diff,
|
|
195
|
+
.gg-log-ns,
|
|
196
|
+
.gg-log-handle,
|
|
197
|
+
.gg-log-content {
|
|
198
|
+
min-width: 0;
|
|
199
|
+
align-self: start !important;
|
|
200
|
+
border-top: 1px solid rgba(0,0,0,0.05);
|
|
201
|
+
}
|
|
202
|
+
.gg-details {
|
|
203
|
+
grid-column: 1 / -1;
|
|
204
|
+
border-top: none;
|
|
205
|
+
}
|
|
198
206
|
.gg-details {
|
|
199
207
|
align-self: stretch !important;
|
|
200
208
|
border-bottom: none;
|
|
@@ -234,19 +242,15 @@ export function createGgPlugin(options, gg) {
|
|
|
234
242
|
word-break: break-word;
|
|
235
243
|
padding: 4px 0;
|
|
236
244
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
.gg-row-filter:hover {
|
|
247
|
-
opacity: 1;
|
|
248
|
-
background: rgba(0,0,0,0.05);
|
|
249
|
-
}
|
|
245
|
+
/* Make header clickable for filtering when filters are expanded */
|
|
246
|
+
.gg-log-header.clickable {
|
|
247
|
+
cursor: pointer;
|
|
248
|
+
}
|
|
249
|
+
/* Desktop: highlight child elements since header has display: contents */
|
|
250
|
+
.gg-log-header.clickable:hover .gg-log-diff,
|
|
251
|
+
.gg-log-header.clickable:hover .gg-log-ns {
|
|
252
|
+
background: rgba(0,0,0,0.05);
|
|
253
|
+
}
|
|
250
254
|
.gg-filter-panel {
|
|
251
255
|
background: #f5f5f5;
|
|
252
256
|
padding: 10px;
|
|
@@ -262,7 +266,7 @@ export function createGgPlugin(options, gg) {
|
|
|
262
266
|
width: 100%;
|
|
263
267
|
padding: 4px 8px;
|
|
264
268
|
font-family: monospace;
|
|
265
|
-
font-size:
|
|
269
|
+
font-size: 16px;
|
|
266
270
|
margin-bottom: 8px;
|
|
267
271
|
}
|
|
268
272
|
.gg-filter-checkboxes {
|
|
@@ -281,16 +285,125 @@ export function createGgPlugin(options, gg) {
|
|
|
281
285
|
font-family: monospace;
|
|
282
286
|
white-space: nowrap;
|
|
283
287
|
}
|
|
288
|
+
/* Mobile responsive styles */
|
|
289
|
+
.gg-toolbar {
|
|
290
|
+
display: flex;
|
|
291
|
+
align-items: center;
|
|
292
|
+
gap: 8px;
|
|
293
|
+
margin-bottom: 8px;
|
|
294
|
+
flex-shrink: 0;
|
|
295
|
+
overflow-x: auto;
|
|
296
|
+
-webkit-overflow-scrolling: touch;
|
|
297
|
+
}
|
|
298
|
+
.gg-toolbar button {
|
|
299
|
+
padding: 4px 10px;
|
|
300
|
+
cursor: pointer;
|
|
301
|
+
flex-shrink: 0;
|
|
302
|
+
}
|
|
303
|
+
.gg-btn-text {
|
|
304
|
+
display: inline;
|
|
305
|
+
}
|
|
306
|
+
.gg-btn-icon {
|
|
307
|
+
display: none;
|
|
308
|
+
}
|
|
309
|
+
@media (max-width: 640px) {
|
|
310
|
+
.gg-btn-text {
|
|
311
|
+
display: none;
|
|
312
|
+
}
|
|
313
|
+
.gg-btn-icon {
|
|
314
|
+
display: inline;
|
|
315
|
+
}
|
|
316
|
+
.gg-toolbar button {
|
|
317
|
+
padding: 4px 8px;
|
|
318
|
+
min-width: 32px;
|
|
319
|
+
}
|
|
320
|
+
.gg-filter-btn {
|
|
321
|
+
font-family: monospace;
|
|
322
|
+
font-size: 12px;
|
|
323
|
+
}
|
|
324
|
+
/* Stack log entries vertically on mobile */
|
|
325
|
+
.gg-log-grid {
|
|
326
|
+
display: block;
|
|
327
|
+
}
|
|
328
|
+
.gg-log-entry {
|
|
329
|
+
display: block;
|
|
330
|
+
padding: 8px 0;
|
|
331
|
+
}
|
|
332
|
+
/* Remove double borders on mobile - only border on entry wrapper */
|
|
333
|
+
.gg-log-entry:not(:first-child) {
|
|
334
|
+
border-top: 1px solid rgba(0,0,0,0.05);
|
|
335
|
+
}
|
|
336
|
+
.gg-log-diff,
|
|
337
|
+
.gg-log-ns,
|
|
338
|
+
.gg-log-handle,
|
|
339
|
+
.gg-log-content,
|
|
340
|
+
.gg-details {
|
|
341
|
+
border-top: none !important;
|
|
342
|
+
}
|
|
343
|
+
.gg-log-header {
|
|
344
|
+
display: flex;
|
|
345
|
+
align-items: center;
|
|
346
|
+
gap: 8px;
|
|
347
|
+
margin-bottom: 4px;
|
|
348
|
+
min-width: 0;
|
|
349
|
+
}
|
|
350
|
+
/* Mobile: hover on container since it's not display: contents */
|
|
351
|
+
.gg-log-header.clickable {
|
|
352
|
+
padding: 2px 0;
|
|
353
|
+
}
|
|
354
|
+
.gg-log-header.clickable:hover {
|
|
355
|
+
background: rgba(0,0,0,0.05);
|
|
356
|
+
}
|
|
357
|
+
/* Override desktop child hover on mobile */
|
|
358
|
+
.gg-log-header.clickable:hover .gg-log-diff,
|
|
359
|
+
.gg-log-header.clickable:hover .gg-log-ns {
|
|
360
|
+
background: transparent;
|
|
361
|
+
}
|
|
362
|
+
.gg-log-diff {
|
|
363
|
+
padding: 0;
|
|
364
|
+
text-align: left;
|
|
365
|
+
flex-shrink: 0;
|
|
366
|
+
white-space: nowrap;
|
|
367
|
+
}
|
|
368
|
+
.gg-log-ns {
|
|
369
|
+
padding: 0;
|
|
370
|
+
flex: 1;
|
|
371
|
+
min-width: 0;
|
|
372
|
+
overflow: hidden;
|
|
373
|
+
text-overflow: ellipsis;
|
|
374
|
+
white-space: nowrap;
|
|
375
|
+
}
|
|
376
|
+
.gg-log-handle {
|
|
377
|
+
display: none;
|
|
378
|
+
}
|
|
379
|
+
.gg-log-content {
|
|
380
|
+
padding: 0;
|
|
381
|
+
padding-left: 0;
|
|
382
|
+
}
|
|
383
|
+
.gg-details {
|
|
384
|
+
margin-top: 4px;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
284
387
|
</style>
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
<
|
|
289
|
-
<
|
|
290
|
-
|
|
291
|
-
|
|
388
|
+
<div class="eruda-gg" style="padding: 10px; height: 100%; display: flex; flex-direction: column; font-size: 14px; touch-action: none; overscroll-behavior: contain;">
|
|
389
|
+
<div class="gg-toolbar">
|
|
390
|
+
<button class="gg-copy-btn">
|
|
391
|
+
<span class="gg-btn-text">Copy</span>
|
|
392
|
+
<span class="gg-btn-icon" title="Copy">📋</span>
|
|
393
|
+
</button>
|
|
394
|
+
<button class="gg-filter-btn" style="text-align: left; white-space: nowrap;">
|
|
395
|
+
<span class="gg-btn-text">Namespaces: </span>
|
|
396
|
+
<span class="gg-btn-icon">NS: </span>
|
|
397
|
+
<span class="gg-filter-summary"></span>
|
|
398
|
+
</button>
|
|
399
|
+
<span class="gg-count" style="opacity: 0.6; white-space: nowrap; flex: 1; text-align: right;"></span>
|
|
400
|
+
<button class="gg-clear-btn">
|
|
401
|
+
<span class="gg-btn-text">Clear</span>
|
|
402
|
+
<span class="gg-btn-icon" title="Clear">⊘</span>
|
|
403
|
+
</button>
|
|
404
|
+
</div>
|
|
292
405
|
<div class="gg-filter-panel"></div>
|
|
293
|
-
<div class="gg-log-container" style="flex: 1; overflow-y: auto; font-family: monospace; font-size: 12px;"></div>
|
|
406
|
+
<div class="gg-log-container" style="flex: 1; overflow-y: auto; font-family: monospace; font-size: 12px; touch-action: pan-y; overscroll-behavior: contain;"></div>
|
|
294
407
|
</div>
|
|
295
408
|
`;
|
|
296
409
|
}
|
|
@@ -340,6 +453,27 @@ export function createGgPlugin(options, gg) {
|
|
|
340
453
|
// Wire up checkboxes
|
|
341
454
|
filterPanel.addEventListener('change', (e) => {
|
|
342
455
|
const target = e.target;
|
|
456
|
+
// Handle ALL checkbox
|
|
457
|
+
if (target.classList.contains('gg-all-checkbox')) {
|
|
458
|
+
const allNamespaces = getAllCapturedNamespaces();
|
|
459
|
+
if (target.checked) {
|
|
460
|
+
// Select all
|
|
461
|
+
filterPattern = 'gg:*';
|
|
462
|
+
enabledNamespaces.clear();
|
|
463
|
+
allNamespaces.forEach((ns) => enabledNamespaces.add(ns));
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
// Deselect all
|
|
467
|
+
const exclusions = allNamespaces.map((ns) => `-${ns}`).join(',');
|
|
468
|
+
filterPattern = `gg:*,${exclusions}`;
|
|
469
|
+
enabledNamespaces.clear();
|
|
470
|
+
}
|
|
471
|
+
localStorage.setItem('debug', filterPattern);
|
|
472
|
+
renderFilterUI();
|
|
473
|
+
renderLogs();
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
// Handle individual namespace checkboxes
|
|
343
477
|
if (target.classList.contains('gg-ns-checkbox')) {
|
|
344
478
|
const namespace = target.getAttribute('data-namespace');
|
|
345
479
|
if (!namespace)
|
|
@@ -355,11 +489,13 @@ export function createGgPlugin(options, gg) {
|
|
|
355
489
|
function renderFilterUI() {
|
|
356
490
|
if (!$el)
|
|
357
491
|
return;
|
|
358
|
-
|
|
492
|
+
const allNamespaces = getAllCapturedNamespaces();
|
|
493
|
+
const enabledCount = enabledNamespaces.size;
|
|
494
|
+
const totalCount = allNamespaces.length;
|
|
495
|
+
// Update button summary with count of enabled namespaces
|
|
359
496
|
const filterSummary = $el.find('.gg-filter-summary').get(0);
|
|
360
497
|
if (filterSummary) {
|
|
361
|
-
|
|
362
|
-
filterSummary.textContent = summary;
|
|
498
|
+
filterSummary.textContent = `${enabledCount}/${totalCount}`;
|
|
363
499
|
}
|
|
364
500
|
// Update panel
|
|
365
501
|
const filterPanel = $el.find('.gg-filter-panel').get(0);
|
|
@@ -374,32 +510,37 @@ export function createGgPlugin(options, gg) {
|
|
|
374
510
|
const effectivePattern = filterPattern || 'gg:*';
|
|
375
511
|
let checkboxesHTML = '';
|
|
376
512
|
if (simple && allNamespaces.length > 0) {
|
|
513
|
+
const allChecked = enabledCount === totalCount;
|
|
377
514
|
checkboxesHTML = `
|
|
378
|
-
|
|
379
|
-
|
|
515
|
+
<div class="gg-filter-checkboxes">
|
|
516
|
+
<label class="gg-filter-checkbox" style="font-weight: bold;">
|
|
517
|
+
<input type="checkbox" class="gg-all-checkbox" ${allChecked ? 'checked' : ''}>
|
|
518
|
+
<span>ALL</span>
|
|
519
|
+
</label>
|
|
520
|
+
${allNamespaces
|
|
380
521
|
.map((ns) => {
|
|
381
522
|
// Check if namespace matches the current pattern
|
|
382
523
|
const checked = namespaceMatchesPattern(ns, effectivePattern);
|
|
383
524
|
return `
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
525
|
+
<label class="gg-filter-checkbox">
|
|
526
|
+
<input type="checkbox" class="gg-ns-checkbox" data-namespace="${escapeHtml(ns)}" ${checked ? 'checked' : ''}>
|
|
527
|
+
<span>${escapeHtml(ns)}</span>
|
|
528
|
+
</label>
|
|
529
|
+
`;
|
|
389
530
|
})
|
|
390
531
|
.join('')}
|
|
391
|
-
|
|
392
|
-
|
|
532
|
+
</div>
|
|
533
|
+
`;
|
|
393
534
|
}
|
|
394
535
|
else if (!simple) {
|
|
395
536
|
checkboxesHTML = `<div style="opacity: 0.6; font-size: 11px; margin: 8px 0;">⚠️ Complex pattern - edit manually (quick filters disabled)</div>`;
|
|
396
537
|
}
|
|
397
538
|
filterPanel.innerHTML = `
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
539
|
+
<div style="margin-bottom: 8px;">
|
|
540
|
+
<input type="text" class="gg-filter-pattern" value="${escapeHtml(filterPattern)}" placeholder="gg:*" style="width: 100%;">
|
|
541
|
+
</div>
|
|
542
|
+
${checkboxesHTML}
|
|
543
|
+
`;
|
|
403
544
|
}
|
|
404
545
|
else {
|
|
405
546
|
// Hide panel
|
|
@@ -429,7 +570,8 @@ export function createGgPlugin(options, gg) {
|
|
|
429
570
|
if (typeof arg === 'object' && arg !== null) {
|
|
430
571
|
return JSON.stringify(arg);
|
|
431
572
|
}
|
|
432
|
-
|
|
573
|
+
// Strip ANSI escape codes from string args
|
|
574
|
+
return stripAnsi(String(arg));
|
|
433
575
|
})
|
|
434
576
|
.join(' ');
|
|
435
577
|
return `${time} ${ns} ${argsStr}`;
|
|
@@ -470,9 +612,12 @@ export function createGgPlugin(options, gg) {
|
|
|
470
612
|
}
|
|
471
613
|
return;
|
|
472
614
|
}
|
|
473
|
-
// Handle
|
|
474
|
-
if
|
|
475
|
-
|
|
615
|
+
// Handle clickable header (when filters expanded)
|
|
616
|
+
// Skip if clicking on resize handle
|
|
617
|
+
if (!target?.classList?.contains('gg-log-handle') &&
|
|
618
|
+
target?.closest('.gg-log-header.clickable')) {
|
|
619
|
+
const header = target.closest('.gg-log-header.clickable');
|
|
620
|
+
const namespace = header.getAttribute('data-namespace');
|
|
476
621
|
if (!namespace)
|
|
477
622
|
return;
|
|
478
623
|
// Toggle this namespace off
|
|
@@ -567,25 +712,40 @@ export function createGgPlugin(options, gg) {
|
|
|
567
712
|
const jsonStr = escapeHtml(JSON.stringify(arg, null, 2));
|
|
568
713
|
const uniqueId = `${index}-${argIdx}`;
|
|
569
714
|
// Store details separately to render after the row
|
|
570
|
-
detailsHTML += `<div class="gg-details" data-index="${uniqueId}" style="display: none;
|
|
715
|
+
detailsHTML += `<div class="gg-details" data-index="${uniqueId}" style="display: none; margin: 4px 0 8px 0; padding: 8px; background: #f8f8f8; border-left: 3px solid ${color}; font-size: 11px; overflow-x: auto;"><pre style="margin: 0;">${jsonStr}</pre></div>`;
|
|
571
716
|
return `<span style="color: #888; cursor: pointer; text-decoration: underline;" class="gg-expand" data-index="${uniqueId}">${preview}</span>`;
|
|
572
717
|
}
|
|
573
718
|
else {
|
|
574
|
-
|
|
719
|
+
// Parse ANSI codes first, then convert URLs to clickable links
|
|
720
|
+
const argStr = String(arg);
|
|
721
|
+
const parsedAnsi = parseAnsiToHtml(argStr);
|
|
722
|
+
// Note: URL linking happens after ANSI parsing, so links work inside colored text
|
|
723
|
+
// This is a simple approach - URLs inside ANSI codes won't be linkified
|
|
724
|
+
// For more complex parsing, we'd need to track ANSI state while matching URLs
|
|
725
|
+
return `<span>${parsedAnsi}</span>`;
|
|
575
726
|
}
|
|
576
727
|
})
|
|
577
728
|
.join(' ');
|
|
578
729
|
}
|
|
579
|
-
//
|
|
580
|
-
const
|
|
581
|
-
|
|
730
|
+
// Make header clickable when filters expanded
|
|
731
|
+
const headerClass = filterExpanded ? 'gg-log-header clickable' : 'gg-log-header';
|
|
732
|
+
const headerAttrs = filterExpanded
|
|
733
|
+
? ` data-namespace="${ns}" title="Click to hide this namespace"`
|
|
582
734
|
: '';
|
|
583
|
-
|
|
584
|
-
|
|
735
|
+
// Add × at start of diff when filters expanded (bold, darker)
|
|
736
|
+
const filterIcon = filterExpanded
|
|
737
|
+
? '<span style="font-weight: bold; color: #000; opacity: 0.6;">× </span>'
|
|
738
|
+
: '';
|
|
739
|
+
// Desktop: grid layout, Mobile: stacked layout
|
|
740
|
+
return (`<div class="gg-log-entry">` +
|
|
741
|
+
`<div class="${headerClass}"${headerAttrs}>` +
|
|
742
|
+
`<div class="gg-log-diff" style="color: ${color};">${filterIcon}${diff}</div>` +
|
|
585
743
|
`<div class="gg-log-ns" style="color: ${color};">${ns}</div>` +
|
|
586
744
|
`<div class="gg-log-handle"></div>` +
|
|
745
|
+
`</div>` +
|
|
587
746
|
`<div class="gg-log-content">${argsHTML}</div>` +
|
|
588
|
-
detailsHTML
|
|
747
|
+
detailsHTML +
|
|
748
|
+
`</div>`);
|
|
589
749
|
})
|
|
590
750
|
.join('')}</div>`;
|
|
591
751
|
logContainer.html(logsHTML);
|
|
@@ -614,5 +774,72 @@ export function createGgPlugin(options, gg) {
|
|
|
614
774
|
div.textContent = text;
|
|
615
775
|
return div.innerHTML;
|
|
616
776
|
}
|
|
777
|
+
/**
|
|
778
|
+
* Strip ANSI escape codes from text
|
|
779
|
+
* Removes all ANSI escape sequences like \x1b[...m
|
|
780
|
+
*/
|
|
781
|
+
function stripAnsi(text) {
|
|
782
|
+
// Remove all ANSI escape codes
|
|
783
|
+
return text.replace(/\x1b\[[0-9;]*m/g, '');
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Parse ANSI escape codes and convert to HTML with inline styles
|
|
787
|
+
* Supports:
|
|
788
|
+
* - 24-bit RGB: \x1b[38;2;r;g;bm (foreground), \x1b[48;2;r;g;bm (background)
|
|
789
|
+
* - Reset: \x1b[0m
|
|
790
|
+
*/
|
|
791
|
+
function parseAnsiToHtml(text) {
|
|
792
|
+
// ANSI escape sequence regex
|
|
793
|
+
// Matches: \x1b[38;2;r;g;bm, \x1b[48;2;r;g;bm, \x1b[0m
|
|
794
|
+
const ansiRegex = /\x1b\[([0-9;]+)m/g;
|
|
795
|
+
let html = '';
|
|
796
|
+
let lastIndex = 0;
|
|
797
|
+
let currentFg = null;
|
|
798
|
+
let currentBg = null;
|
|
799
|
+
let match;
|
|
800
|
+
while ((match = ansiRegex.exec(text)) !== null) {
|
|
801
|
+
// Add text before this code (with current styling)
|
|
802
|
+
const textBefore = text.slice(lastIndex, match.index);
|
|
803
|
+
if (textBefore) {
|
|
804
|
+
html += wrapWithStyle(escapeHtml(textBefore), currentFg, currentBg);
|
|
805
|
+
}
|
|
806
|
+
// Parse the ANSI code
|
|
807
|
+
const code = match[1];
|
|
808
|
+
const parts = code.split(';').map(Number);
|
|
809
|
+
if (parts[0] === 0) {
|
|
810
|
+
// Reset
|
|
811
|
+
currentFg = null;
|
|
812
|
+
currentBg = null;
|
|
813
|
+
}
|
|
814
|
+
else if (parts[0] === 38 && parts[1] === 2 && parts.length >= 5) {
|
|
815
|
+
// Foreground RGB: 38;2;r;g;b
|
|
816
|
+
currentFg = `rgb(${parts[2]},${parts[3]},${parts[4]})`;
|
|
817
|
+
}
|
|
818
|
+
else if (parts[0] === 48 && parts[1] === 2 && parts.length >= 5) {
|
|
819
|
+
// Background RGB: 48;2;r;g;b
|
|
820
|
+
currentBg = `rgb(${parts[2]},${parts[3]},${parts[4]})`;
|
|
821
|
+
}
|
|
822
|
+
lastIndex = ansiRegex.lastIndex;
|
|
823
|
+
}
|
|
824
|
+
// Add remaining text
|
|
825
|
+
const remaining = text.slice(lastIndex);
|
|
826
|
+
if (remaining) {
|
|
827
|
+
html += wrapWithStyle(escapeHtml(remaining), currentFg, currentBg);
|
|
828
|
+
}
|
|
829
|
+
return html || escapeHtml(text);
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Wrap text with inline color styles
|
|
833
|
+
*/
|
|
834
|
+
function wrapWithStyle(text, fg, bg) {
|
|
835
|
+
if (!fg && !bg)
|
|
836
|
+
return text;
|
|
837
|
+
const styles = [];
|
|
838
|
+
if (fg)
|
|
839
|
+
styles.push(`color: ${fg}`);
|
|
840
|
+
if (bg)
|
|
841
|
+
styles.push(`background-color: ${bg}`);
|
|
842
|
+
return `<span style="${styles.join('; ')}">${text}</span>`;
|
|
843
|
+
}
|
|
617
844
|
return plugin;
|
|
618
845
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
export interface GgTagPluginOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Pattern to strip from file paths to produce short callpoints.
|
|
5
|
+
* Should match up to and including the source root folder.
|
|
6
|
+
*
|
|
7
|
+
* Default: /.*?(\/(?:src|chunks)\/)/ which strips everything up to "src/" or "chunks/",
|
|
8
|
+
* matching the dev-mode behavior of gg().
|
|
9
|
+
*
|
|
10
|
+
* Example: "/Users/me/project/src/routes/+page.svelte" → "routes/+page.svelte"
|
|
11
|
+
*/
|
|
12
|
+
srcRootPattern?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Vite plugin that rewrites bare `gg(...)` calls to `gg.ns('callpoint', ...)`
|
|
16
|
+
* at build time. This gives each call site a unique namespace with zero runtime
|
|
17
|
+
* cost — no stack trace parsing needed.
|
|
18
|
+
*
|
|
19
|
+
* Works in both dev and prod. When the plugin is installed, `gg.ns()` is called
|
|
20
|
+
* with the callpoint baked in as a string literal. Without the plugin, gg()
|
|
21
|
+
* falls back to runtime stack parsing in dev and bare `gg:` in prod.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* // vite.config.ts
|
|
25
|
+
* import { ggTagPlugin } from '@leftium/gg';
|
|
26
|
+
*
|
|
27
|
+
* export default defineConfig({
|
|
28
|
+
* plugins: [ggTagPlugin()]
|
|
29
|
+
* });
|
|
30
|
+
*/
|
|
31
|
+
export default function ggTagPlugin(options?: GgTagPluginOptions): Plugin;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vite plugin that rewrites bare `gg(...)` calls to `gg.ns('callpoint', ...)`
|
|
3
|
+
* at build time. This gives each call site a unique namespace with zero runtime
|
|
4
|
+
* cost — no stack trace parsing needed.
|
|
5
|
+
*
|
|
6
|
+
* Works in both dev and prod. When the plugin is installed, `gg.ns()` is called
|
|
7
|
+
* with the callpoint baked in as a string literal. Without the plugin, gg()
|
|
8
|
+
* falls back to runtime stack parsing in dev and bare `gg:` in prod.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* // vite.config.ts
|
|
12
|
+
* import { ggTagPlugin } from '@leftium/gg';
|
|
13
|
+
*
|
|
14
|
+
* export default defineConfig({
|
|
15
|
+
* plugins: [ggTagPlugin()]
|
|
16
|
+
* });
|
|
17
|
+
*/
|
|
18
|
+
export default function ggTagPlugin(options = {}) {
|
|
19
|
+
const srcRootPattern = options.srcRootPattern ?? '.*?(/(?:src|chunks)/)';
|
|
20
|
+
const srcRootRegex = new RegExp(srcRootPattern, 'i');
|
|
21
|
+
return {
|
|
22
|
+
name: 'gg-tag',
|
|
23
|
+
transform(code, id) {
|
|
24
|
+
// Only process JS/TS/Svelte files
|
|
25
|
+
if (!/\.(js|ts|svelte|jsx|tsx|mjs|mts)(\?.*)?$/.test(id))
|
|
26
|
+
return null;
|
|
27
|
+
// Quick bail: no gg calls in this file
|
|
28
|
+
if (!code.includes('gg('))
|
|
29
|
+
return null;
|
|
30
|
+
// Don't transform gg's own source files
|
|
31
|
+
if (id.includes('/lib/gg.') || id.includes('/lib/debug'))
|
|
32
|
+
return null;
|
|
33
|
+
// Build the short callpoint from the file path
|
|
34
|
+
// e.g. "/Users/me/project/src/routes/+page.svelte" → "routes/+page.svelte"
|
|
35
|
+
const shortPath = id.replace(srcRootRegex, '');
|
|
36
|
+
return transformGgCalls(code, shortPath);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Find the enclosing function name for a given position in source code.
|
|
42
|
+
* Scans backwards from the position looking for function/method declarations.
|
|
43
|
+
*/
|
|
44
|
+
function findEnclosingFunction(code, position) {
|
|
45
|
+
// Look backwards from the gg( call for the nearest function declaration
|
|
46
|
+
const before = code.slice(0, position);
|
|
47
|
+
// Try several patterns, take the closest (last) match
|
|
48
|
+
// Named function: function handleClick(
|
|
49
|
+
// Arrow in variable: const handleClick = (...) =>
|
|
50
|
+
// Arrow in variable: let handleClick = (...) =>
|
|
51
|
+
// Method shorthand: handleClick() {
|
|
52
|
+
// Method: handleClick: function(
|
|
53
|
+
// Class method: async handleClick(
|
|
54
|
+
const patterns = [
|
|
55
|
+
// function declarations: function foo(
|
|
56
|
+
/function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g,
|
|
57
|
+
// const/let/var assignment to arrow or function: const foo =
|
|
58
|
+
/(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=/g,
|
|
59
|
+
// object method shorthand: foo() { or async foo() {
|
|
60
|
+
/(?:async\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\([^)]*\)\s*\{/g,
|
|
61
|
+
// object property function: foo: function
|
|
62
|
+
/([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:\s*(?:async\s+)?function/g
|
|
63
|
+
];
|
|
64
|
+
let closestName = '';
|
|
65
|
+
let closestPos = -1;
|
|
66
|
+
for (const pattern of patterns) {
|
|
67
|
+
let match;
|
|
68
|
+
while ((match = pattern.exec(before)) !== null) {
|
|
69
|
+
const name = match[1];
|
|
70
|
+
// Skip common false positives
|
|
71
|
+
if ([
|
|
72
|
+
'if',
|
|
73
|
+
'for',
|
|
74
|
+
'while',
|
|
75
|
+
'switch',
|
|
76
|
+
'catch',
|
|
77
|
+
'return',
|
|
78
|
+
'import',
|
|
79
|
+
'export',
|
|
80
|
+
'from',
|
|
81
|
+
'new',
|
|
82
|
+
'typeof',
|
|
83
|
+
'instanceof',
|
|
84
|
+
'void',
|
|
85
|
+
'delete',
|
|
86
|
+
'throw',
|
|
87
|
+
'case',
|
|
88
|
+
'else',
|
|
89
|
+
'in',
|
|
90
|
+
'of',
|
|
91
|
+
'do',
|
|
92
|
+
'try',
|
|
93
|
+
'class',
|
|
94
|
+
'super',
|
|
95
|
+
'this',
|
|
96
|
+
'with',
|
|
97
|
+
'yield',
|
|
98
|
+
'await',
|
|
99
|
+
'debugger',
|
|
100
|
+
'default'
|
|
101
|
+
].includes(name)) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (match.index > closestPos) {
|
|
105
|
+
closestPos = match.index;
|
|
106
|
+
closestName = name;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return closestName;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Transform gg() calls in source code to gg.ns('callpoint', ...) calls.
|
|
114
|
+
*
|
|
115
|
+
* Handles:
|
|
116
|
+
* - bare gg(...) → gg.ns('callpoint', ...)
|
|
117
|
+
* - gg.ns(...) → left untouched (user-specified namespace)
|
|
118
|
+
* - gg.enable, gg.disable, gg.clearPersist, gg._onLog → left untouched
|
|
119
|
+
* - gg inside strings and comments → left untouched
|
|
120
|
+
*/
|
|
121
|
+
function transformGgCalls(code, shortPath) {
|
|
122
|
+
// Match gg( that is:
|
|
123
|
+
// - not preceded by a dot (would be obj.gg() — not our function)
|
|
124
|
+
// - not preceded by a word char (would be dogg() or something)
|
|
125
|
+
// - not followed by a dot before the paren (gg.ns, gg.enable, etc.)
|
|
126
|
+
//
|
|
127
|
+
// We use a manual scan approach to correctly handle strings and comments.
|
|
128
|
+
const result = [];
|
|
129
|
+
let lastIndex = 0;
|
|
130
|
+
let modified = false;
|
|
131
|
+
// States for string/comment tracking
|
|
132
|
+
let i = 0;
|
|
133
|
+
while (i < code.length) {
|
|
134
|
+
// Skip single-line comments
|
|
135
|
+
if (code[i] === '/' && code[i + 1] === '/') {
|
|
136
|
+
const end = code.indexOf('\n', i);
|
|
137
|
+
i = end === -1 ? code.length : end + 1;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
// Skip multi-line comments
|
|
141
|
+
if (code[i] === '/' && code[i + 1] === '*') {
|
|
142
|
+
const end = code.indexOf('*/', i + 2);
|
|
143
|
+
i = end === -1 ? code.length : end + 2;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
// Skip template literals (backticks)
|
|
147
|
+
if (code[i] === '`') {
|
|
148
|
+
i++;
|
|
149
|
+
let depth = 0;
|
|
150
|
+
while (i < code.length) {
|
|
151
|
+
if (code[i] === '\\') {
|
|
152
|
+
i += 2;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (code[i] === '$' && code[i + 1] === '{') {
|
|
156
|
+
depth++;
|
|
157
|
+
i += 2;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (code[i] === '}' && depth > 0) {
|
|
161
|
+
depth--;
|
|
162
|
+
i++;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (code[i] === '`' && depth === 0) {
|
|
166
|
+
i++;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
i++;
|
|
170
|
+
}
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
// Skip strings (single and double quotes)
|
|
174
|
+
if (code[i] === '"' || code[i] === "'") {
|
|
175
|
+
const quote = code[i];
|
|
176
|
+
i++;
|
|
177
|
+
while (i < code.length) {
|
|
178
|
+
if (code[i] === '\\') {
|
|
179
|
+
i += 2;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (code[i] === quote) {
|
|
183
|
+
i++;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
i++;
|
|
187
|
+
}
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
// Look for 'gg(' pattern
|
|
191
|
+
if (code[i] === 'g' && code[i + 1] === 'g' && code[i + 2] === '(') {
|
|
192
|
+
// Check preceding character: must not be a word char or dot
|
|
193
|
+
const prevChar = i > 0 ? code[i - 1] : '';
|
|
194
|
+
if (prevChar && /[a-zA-Z0-9_$.]/.test(prevChar)) {
|
|
195
|
+
i++;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
// Check it's not gg.something (gg.ns, gg.enable, etc.)
|
|
199
|
+
// At this point we know code[i..i+2] is "gg(" — it's a bare call
|
|
200
|
+
// Find the enclosing function
|
|
201
|
+
const fnName = findEnclosingFunction(code, i);
|
|
202
|
+
const callpoint = `${shortPath}${fnName ? `@${fnName}` : ''}`;
|
|
203
|
+
const escaped = callpoint.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
204
|
+
// Emit everything before this match
|
|
205
|
+
result.push(code.slice(lastIndex, i));
|
|
206
|
+
// Replace gg( with gg.ns('callpoint',
|
|
207
|
+
// Need to handle gg() with no args → gg.ns('callpoint')
|
|
208
|
+
// and gg(x) → gg.ns('callpoint', x)
|
|
209
|
+
// Peek ahead to check if it's gg() with no args
|
|
210
|
+
const afterParen = code.indexOf(')', i + 3);
|
|
211
|
+
const betweenParens = code.slice(i + 3, afterParen);
|
|
212
|
+
const isNoArgs = betweenParens.trim() === '';
|
|
213
|
+
if (isNoArgs && afterParen !== -1 && !betweenParens.includes('(')) {
|
|
214
|
+
// gg() → gg.ns('callpoint')
|
|
215
|
+
result.push(`gg.ns('${escaped}')`);
|
|
216
|
+
lastIndex = afterParen + 1;
|
|
217
|
+
i = afterParen + 1;
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
// gg(args...) → gg.ns('callpoint', args...)
|
|
221
|
+
result.push(`gg.ns('${escaped}', `);
|
|
222
|
+
lastIndex = i + 3; // skip past "gg("
|
|
223
|
+
i = i + 3;
|
|
224
|
+
}
|
|
225
|
+
modified = true;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
i++;
|
|
229
|
+
}
|
|
230
|
+
if (!modified)
|
|
231
|
+
return null;
|
|
232
|
+
result.push(code.slice(lastIndex));
|
|
233
|
+
return { code: result.join(''), map: null };
|
|
234
|
+
}
|
package/dist/gg.d.ts
CHANGED
|
@@ -22,6 +22,73 @@ export declare namespace gg {
|
|
|
22
22
|
var disable: () => void;
|
|
23
23
|
var enable: (namespaces: string) => void;
|
|
24
24
|
var clearPersist: () => void;
|
|
25
|
-
var _onLog: OnLogCallback | null;
|
|
26
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* ANSI Color Helpers for gg()
|
|
28
|
+
*
|
|
29
|
+
* Create reusable color schemes with foreground (fg) and background (bg) colors.
|
|
30
|
+
* Works in both native console and Eruda plugin.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* // Method chaining (order doesn't matter)
|
|
34
|
+
* gg(fg('white').bg('red')`Critical error!`);
|
|
35
|
+
* gg(bg('green').fg('white')`Success!`);
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* // Define color schemes once, reuse everywhere
|
|
39
|
+
* const input = fg('blue').bg('yellow');
|
|
40
|
+
* const transcript = bg('green').fg('white');
|
|
41
|
+
* const error = fg('white').bg('red');
|
|
42
|
+
*
|
|
43
|
+
* gg(input`User said: hello`);
|
|
44
|
+
* gg(transcript`AI responded: hi`);
|
|
45
|
+
* gg(error`Something broke!`);
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* // Mix colored and normal text inline
|
|
49
|
+
* gg(fg('red')`Error: ` + bg('yellow')`warning` + ' normal text');
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* // Custom colors (hex, rgb, or named)
|
|
53
|
+
* gg(fg('#ff6347').bg('#98fb98')`Custom colors`);
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* // Just foreground or background
|
|
57
|
+
* gg(fg('cyan')`Cyan text`);
|
|
58
|
+
* gg(bg('magenta')`Magenta background`);
|
|
59
|
+
*/
|
|
60
|
+
type ColorTagFunction = (strings: TemplateStringsArray, ...values: unknown[]) => string;
|
|
61
|
+
interface ChainableColorFn extends ColorTagFunction {
|
|
62
|
+
fg: (color: string) => ChainableColorFn;
|
|
63
|
+
bg: (color: string) => ChainableColorFn;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Foreground (text) color helper
|
|
67
|
+
* Can be used directly or chained with .bg()
|
|
68
|
+
*
|
|
69
|
+
* @param color - Named color, hex (#rrggbb), or rgb(r,g,b)
|
|
70
|
+
* @example
|
|
71
|
+
* gg(fg('red')`Error`);
|
|
72
|
+
* gg(fg('white').bg('red')`Critical!`);
|
|
73
|
+
*/
|
|
74
|
+
export declare function fg(color: string): ChainableColorFn;
|
|
75
|
+
/**
|
|
76
|
+
* Background color helper
|
|
77
|
+
* Can be used directly or chained with .fg()
|
|
78
|
+
*
|
|
79
|
+
* @param color - Named color, hex (#rrggbb), or rgb(r,g,b)
|
|
80
|
+
* @example
|
|
81
|
+
* gg(bg('yellow')`Warning`);
|
|
82
|
+
* gg(bg('green').fg('white')`Success!`);
|
|
83
|
+
*/
|
|
84
|
+
export declare function bg(color: string): ChainableColorFn;
|
|
85
|
+
export declare namespace gg {
|
|
86
|
+
let _onLog: OnLogCallback | null;
|
|
87
|
+
let ns: (nsLabel: string, ...args: unknown[]) => unknown;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Run gg diagnostics and log configuration status
|
|
91
|
+
* Can be called immediately or delayed (e.g., after Eruda loads)
|
|
92
|
+
*/
|
|
93
|
+
export declare function runGgDiagnostics(): Promise<void>;
|
|
27
94
|
export {};
|
package/dist/gg.js
CHANGED
|
@@ -209,38 +209,117 @@ export function gg(...args) {
|
|
|
209
209
|
}
|
|
210
210
|
const ggLogFunction = namespaceToLogFunction.get(namespace) ||
|
|
211
211
|
namespaceToLogFunction.set(namespace, createGgDebugger(namespace)).get(namespace);
|
|
212
|
+
// Prepare args for logging
|
|
213
|
+
let logArgs;
|
|
214
|
+
let returnValue;
|
|
212
215
|
if (!args.length) {
|
|
213
|
-
|
|
214
|
-
|
|
216
|
+
// No arguments: log editor link
|
|
217
|
+
logArgs = [` 📝📝 ${url} 👀👀`];
|
|
218
|
+
returnValue = {
|
|
215
219
|
fileName,
|
|
216
220
|
functionName,
|
|
217
221
|
url,
|
|
218
222
|
stack
|
|
219
223
|
};
|
|
220
224
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
225
|
+
else if (args.length === 1) {
|
|
226
|
+
logArgs = [args[0]];
|
|
227
|
+
returnValue = args[0];
|
|
224
228
|
}
|
|
225
229
|
else {
|
|
226
|
-
|
|
230
|
+
logArgs = [args[0], ...args.slice(1)];
|
|
231
|
+
returnValue = args[0];
|
|
232
|
+
}
|
|
233
|
+
// Log to console via debug
|
|
234
|
+
if (logArgs.length === 1) {
|
|
235
|
+
ggLogFunction(logArgs[0]);
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
ggLogFunction(logArgs[0], ...logArgs.slice(1));
|
|
227
239
|
}
|
|
228
240
|
// Call capture hook if registered (for Eruda plugin)
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
+
const entry = {
|
|
242
|
+
namespace,
|
|
243
|
+
color: ggLogFunction.color,
|
|
244
|
+
diff: ggLogFunction.diff || 0, // Millisecond diff from debug library
|
|
245
|
+
message: logArgs.length === 1 ? String(logArgs[0]) : logArgs.map(String).join(' '),
|
|
246
|
+
args: logArgs, // Keep raw args for object inspection
|
|
247
|
+
timestamp: Date.now()
|
|
248
|
+
};
|
|
249
|
+
if (_onLogCallback) {
|
|
250
|
+
_onLogCallback(entry);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
// Buffer early logs before Eruda initializes
|
|
254
|
+
earlyLogBuffer.push(entry);
|
|
241
255
|
}
|
|
242
|
-
return
|
|
256
|
+
return returnValue;
|
|
243
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* gg.ns() - Log with an explicit namespace (callpoint label).
|
|
260
|
+
*
|
|
261
|
+
* In production builds, the ggTagPlugin Vite plugin rewrites bare gg() calls
|
|
262
|
+
* to gg.ns('callpoint', ...) so each call site gets a unique namespace even
|
|
263
|
+
* after minification. Users can also call gg.ns() directly to set a meaningful
|
|
264
|
+
* label that survives across builds.
|
|
265
|
+
*
|
|
266
|
+
* @param nsLabel - The namespace label (appears as gg:<nsLabel> in output)
|
|
267
|
+
* @param args - Same arguments as gg()
|
|
268
|
+
* @returns Same as gg() - the first arg, or call-site info if no args
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* gg.ns("auth", "login failed") // logs under namespace "gg:auth"
|
|
272
|
+
* gg.ns("cart", item, quantity) // logs under namespace "gg:cart"
|
|
273
|
+
*/
|
|
274
|
+
gg.ns = function (nsLabel, ...args) {
|
|
275
|
+
if (!ggConfig.enabled || isCloudflareWorker()) {
|
|
276
|
+
return args.length ? args[0] : { url: '', stack: [] };
|
|
277
|
+
}
|
|
278
|
+
const namespace = `gg:${nsLabel}`;
|
|
279
|
+
if (nsLabel.length < 80 && nsLabel.length > maxCallpointLength) {
|
|
280
|
+
maxCallpointLength = nsLabel.length;
|
|
281
|
+
}
|
|
282
|
+
const ggLogFunction = namespaceToLogFunction.get(namespace) ||
|
|
283
|
+
namespaceToLogFunction.set(namespace, createGgDebugger(namespace)).get(namespace);
|
|
284
|
+
// Prepare args for logging
|
|
285
|
+
let logArgs;
|
|
286
|
+
let returnValue;
|
|
287
|
+
if (!args.length) {
|
|
288
|
+
logArgs = [` 📝 ${nsLabel}`];
|
|
289
|
+
returnValue = { fileName: '', functionName: '', url: '', stack: [] };
|
|
290
|
+
}
|
|
291
|
+
else if (args.length === 1) {
|
|
292
|
+
logArgs = [args[0]];
|
|
293
|
+
returnValue = args[0];
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
logArgs = [args[0], ...args.slice(1)];
|
|
297
|
+
returnValue = args[0];
|
|
298
|
+
}
|
|
299
|
+
// Log to console via debug
|
|
300
|
+
if (logArgs.length === 1) {
|
|
301
|
+
ggLogFunction(logArgs[0]);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
ggLogFunction(logArgs[0], ...logArgs.slice(1));
|
|
305
|
+
}
|
|
306
|
+
// Call capture hook if registered (for Eruda plugin)
|
|
307
|
+
const entry = {
|
|
308
|
+
namespace,
|
|
309
|
+
color: ggLogFunction.color,
|
|
310
|
+
diff: ggLogFunction.diff || 0,
|
|
311
|
+
message: logArgs.length === 1 ? String(logArgs[0]) : logArgs.map(String).join(' '),
|
|
312
|
+
args: logArgs,
|
|
313
|
+
timestamp: Date.now()
|
|
314
|
+
};
|
|
315
|
+
if (_onLogCallback) {
|
|
316
|
+
_onLogCallback(entry);
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
earlyLogBuffer.push(entry);
|
|
320
|
+
}
|
|
321
|
+
return returnValue;
|
|
322
|
+
};
|
|
244
323
|
gg.disable = isCloudflareWorker() ? () => { } : debugFactory.disable;
|
|
245
324
|
gg.enable = isCloudflareWorker() ? () => { } : debugFactory.enable;
|
|
246
325
|
/**
|
|
@@ -258,25 +337,165 @@ gg.clearPersist = () => {
|
|
|
258
337
|
}
|
|
259
338
|
}
|
|
260
339
|
};
|
|
340
|
+
/**
|
|
341
|
+
* Parse color string to RGB values
|
|
342
|
+
* Accepts: named colors, hex (#rgb, #rrggbb), rgb(r,g,b), rgba(r,g,b,a)
|
|
343
|
+
*/
|
|
344
|
+
function parseColor(color) {
|
|
345
|
+
// Named colors map (basic ANSI colors + common web colors)
|
|
346
|
+
const namedColors = {
|
|
347
|
+
black: '#000000',
|
|
348
|
+
red: '#ff0000',
|
|
349
|
+
green: '#00ff00',
|
|
350
|
+
yellow: '#ffff00',
|
|
351
|
+
blue: '#0000ff',
|
|
352
|
+
magenta: '#ff00ff',
|
|
353
|
+
cyan: '#00ffff',
|
|
354
|
+
white: '#ffffff',
|
|
355
|
+
// Bright variants
|
|
356
|
+
brightBlack: '#808080',
|
|
357
|
+
brightRed: '#ff6666',
|
|
358
|
+
brightGreen: '#66ff66',
|
|
359
|
+
brightYellow: '#ffff66',
|
|
360
|
+
brightBlue: '#6666ff',
|
|
361
|
+
brightMagenta: '#ff66ff',
|
|
362
|
+
brightCyan: '#66ffff',
|
|
363
|
+
brightWhite: '#ffffff',
|
|
364
|
+
// Common aliases
|
|
365
|
+
gray: '#808080',
|
|
366
|
+
grey: '#808080',
|
|
367
|
+
orange: '#ffa500',
|
|
368
|
+
purple: '#800080',
|
|
369
|
+
pink: '#ffc0cb'
|
|
370
|
+
};
|
|
371
|
+
// Check named colors first
|
|
372
|
+
const normalized = color.toLowerCase().trim();
|
|
373
|
+
if (namedColors[normalized]) {
|
|
374
|
+
color = namedColors[normalized];
|
|
375
|
+
}
|
|
376
|
+
// Parse hex color
|
|
377
|
+
const hexMatch = color.match(/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i);
|
|
378
|
+
if (hexMatch) {
|
|
379
|
+
return {
|
|
380
|
+
r: parseInt(hexMatch[1], 16),
|
|
381
|
+
g: parseInt(hexMatch[2], 16),
|
|
382
|
+
b: parseInt(hexMatch[3], 16)
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
// Parse short hex (#rgb)
|
|
386
|
+
const shortHexMatch = color.match(/^#?([a-f\d])([a-f\d])([a-f\d])$/i);
|
|
387
|
+
if (shortHexMatch) {
|
|
388
|
+
return {
|
|
389
|
+
r: parseInt(shortHexMatch[1] + shortHexMatch[1], 16),
|
|
390
|
+
g: parseInt(shortHexMatch[2] + shortHexMatch[2], 16),
|
|
391
|
+
b: parseInt(shortHexMatch[3] + shortHexMatch[3], 16)
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
// Parse rgb(r,g,b) or rgba(r,g,b,a)
|
|
395
|
+
const rgbMatch = color.match(/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i);
|
|
396
|
+
if (rgbMatch) {
|
|
397
|
+
return {
|
|
398
|
+
r: parseInt(rgbMatch[1]),
|
|
399
|
+
g: parseInt(rgbMatch[2]),
|
|
400
|
+
b: parseInt(rgbMatch[3])
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Internal helper to create chainable color function with method chaining
|
|
407
|
+
*/
|
|
408
|
+
function createColorFunction(fgCode = '', bgCode = '') {
|
|
409
|
+
const tagFn = function (strings, ...values) {
|
|
410
|
+
const text = strings.reduce((acc, str, i) => acc + str + (values[i] !== undefined ? String(values[i]) : ''), '');
|
|
411
|
+
return fgCode + bgCode + text + '\x1b[0m';
|
|
412
|
+
};
|
|
413
|
+
// Add method chaining
|
|
414
|
+
tagFn.fg = (color) => {
|
|
415
|
+
const rgb = parseColor(color);
|
|
416
|
+
const newFgCode = rgb ? `\x1b[38;2;${rgb.r};${rgb.g};${rgb.b}m` : '';
|
|
417
|
+
return createColorFunction(newFgCode, bgCode);
|
|
418
|
+
};
|
|
419
|
+
tagFn.bg = (color) => {
|
|
420
|
+
const rgb = parseColor(color);
|
|
421
|
+
const newBgCode = rgb ? `\x1b[48;2;${rgb.r};${rgb.g};${rgb.b}m` : '';
|
|
422
|
+
return createColorFunction(fgCode, newBgCode);
|
|
423
|
+
};
|
|
424
|
+
return tagFn;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Foreground (text) color helper
|
|
428
|
+
* Can be used directly or chained with .bg()
|
|
429
|
+
*
|
|
430
|
+
* @param color - Named color, hex (#rrggbb), or rgb(r,g,b)
|
|
431
|
+
* @example
|
|
432
|
+
* gg(fg('red')`Error`);
|
|
433
|
+
* gg(fg('white').bg('red')`Critical!`);
|
|
434
|
+
*/
|
|
435
|
+
export function fg(color) {
|
|
436
|
+
const rgb = parseColor(color);
|
|
437
|
+
const fgCode = rgb ? `\x1b[38;2;${rgb.r};${rgb.g};${rgb.b}m` : '';
|
|
438
|
+
return createColorFunction(fgCode, '');
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Background color helper
|
|
442
|
+
* Can be used directly or chained with .fg()
|
|
443
|
+
*
|
|
444
|
+
* @param color - Named color, hex (#rrggbb), or rgb(r,g,b)
|
|
445
|
+
* @example
|
|
446
|
+
* gg(bg('yellow')`Warning`);
|
|
447
|
+
* gg(bg('green').fg('white')`Success!`);
|
|
448
|
+
*/
|
|
449
|
+
export function bg(color) {
|
|
450
|
+
const rgb = parseColor(color);
|
|
451
|
+
const bgCode = rgb ? `\x1b[48;2;${rgb.r};${rgb.g};${rgb.b}m` : '';
|
|
452
|
+
return createColorFunction('', bgCode);
|
|
453
|
+
}
|
|
261
454
|
/**
|
|
262
455
|
* Hook for capturing gg() output (used by Eruda plugin)
|
|
263
456
|
* Set this to a callback function to receive log entries
|
|
264
457
|
*/
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
458
|
+
// Buffer for capturing early logs before Eruda initializes
|
|
459
|
+
const earlyLogBuffer = [];
|
|
460
|
+
let _onLogCallback = null;
|
|
461
|
+
// Proxy property that replays buffered logs when hook is registered
|
|
462
|
+
Object.defineProperty(gg, '_onLog', {
|
|
463
|
+
get() {
|
|
464
|
+
return _onLogCallback;
|
|
465
|
+
},
|
|
466
|
+
set(callback) {
|
|
467
|
+
_onLogCallback = callback;
|
|
468
|
+
// Replay buffered logs when callback is first registered
|
|
469
|
+
if (callback && earlyLogBuffer.length > 0) {
|
|
470
|
+
earlyLogBuffer.forEach((entry) => callback(entry));
|
|
471
|
+
earlyLogBuffer.length = 0; // Clear buffer after replay
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
// Namespace for adding properties to the gg function
|
|
476
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
477
|
+
(function (gg) {
|
|
478
|
+
})(gg || (gg = {}));
|
|
479
|
+
/**
|
|
480
|
+
* Run gg diagnostics and log configuration status
|
|
481
|
+
* Can be called immediately or delayed (e.g., after Eruda loads)
|
|
482
|
+
*/
|
|
483
|
+
export async function runGgDiagnostics() {
|
|
484
|
+
if (!ggConfig.showHints || isCloudflareWorker())
|
|
485
|
+
return;
|
|
268
486
|
const ggLogTest = debugFactory('gg:TEST');
|
|
269
487
|
let ggMessage = '\n';
|
|
270
488
|
// Utilities for forming ggMessage:
|
|
271
489
|
const message = (s) => (ggMessage += `${s}\n`);
|
|
272
490
|
const checkbox = (test) => (test ? '✅' : '❌');
|
|
273
491
|
const makeHint = (test, ifTrue, ifFalse = '') => (test ? ifTrue : ifFalse);
|
|
492
|
+
// Use plain console.log for diagnostics - appears in Eruda's Console tab
|
|
274
493
|
console.log(`Loaded gg module. Checking configuration...`);
|
|
275
494
|
if (ggConfig.enabled && ggLogTest.enabled) {
|
|
276
495
|
gg('If you can see this logg, gg configured correctly!');
|
|
277
496
|
message(`No problems detected:`);
|
|
278
497
|
if (BROWSER) {
|
|
279
|
-
message(`ℹ️ If gg output
|
|
498
|
+
message(`ℹ️ If gg output not visible: enable "Verbose" log level in DevTools, or check Eruda's GG tab.`);
|
|
280
499
|
}
|
|
281
500
|
}
|
|
282
501
|
else {
|
|
@@ -307,8 +526,18 @@ if (ggConfig.showHints && !isCloudflareWorker()) {
|
|
|
307
526
|
}
|
|
308
527
|
message(`${checkbox(ggLogTest.enabled)} DEBUG env variable: ${process?.env?.DEBUG}${hint}`);
|
|
309
528
|
}
|
|
529
|
+
// Use plain console.log for diagnostics - appears in Eruda's Console tab
|
|
310
530
|
console.log(ggMessage);
|
|
311
531
|
// Reset namespace width after configuration check
|
|
312
532
|
// This prevents the long callpoint from the config check from affecting subsequent logs
|
|
313
533
|
resetNamespaceWidth();
|
|
314
534
|
}
|
|
535
|
+
// Run diagnostics immediately on module load if Eruda is not being used
|
|
536
|
+
// (If Eruda will load, the loader will call runGgDiagnostics after Eruda is ready)
|
|
537
|
+
if (ggConfig.showHints && !isCloudflareWorker()) {
|
|
538
|
+
// Only run immediately if we're not in a context where Eruda might load
|
|
539
|
+
// In browser dev mode, assume Eruda might load and skip immediate diagnostics
|
|
540
|
+
if (!BROWSER || !DEV) {
|
|
541
|
+
runGgDiagnostics();
|
|
542
|
+
}
|
|
543
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Reexport your entry components here
|
|
2
|
-
import { gg } from './gg.js';
|
|
2
|
+
import { gg, fg, bg } from './gg.js';
|
|
3
3
|
import openInEditorPlugin from './open-in-editor.js';
|
|
4
|
-
|
|
4
|
+
import ggTagPlugin from './gg-tag-plugin.js';
|
|
5
|
+
export { gg, fg, bg, openInEditorPlugin, ggTagPlugin };
|