@leftium/gg 0.0.34 → 0.0.36
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 +25 -19
- package/dist/OpenInEditorLink.svelte +17 -7
- package/dist/OpenInEditorLink.svelte.d.ts +8 -2
- package/dist/eruda/plugin.js +108 -4
- package/dist/eruda/types.d.ts +11 -0
- package/dist/gg-call-sites-plugin.js +106 -52
- package/dist/gg.d.ts +81 -0
- package/dist/gg.js +410 -59
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,7 +20,27 @@ npm add @leftium/gg
|
|
|
20
20
|
|
|
21
21
|
## SvelteKit Quick Start
|
|
22
22
|
|
|
23
|
-
### 1.
|
|
23
|
+
### 1. Use `gg()` anywhere
|
|
24
|
+
|
|
25
|
+
```svelte
|
|
26
|
+
<script>
|
|
27
|
+
import { gg } from '@leftium/gg';
|
|
28
|
+
|
|
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
|
+
</script>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
That's it! Output appears in the browser dev console and terminal. The following optional steps are highly recommended to unlock the full experience:
|
|
40
|
+
|
|
41
|
+
### 2. Add Vite plugins (optional, recommended)
|
|
42
|
+
|
|
43
|
+
Without plugins, namespaces are random word-tuples. With plugins, you get real file/function callpoints, open-in-editor links, and icecream-style source expressions.
|
|
24
44
|
|
|
25
45
|
```ts
|
|
26
46
|
// vite.config.ts
|
|
@@ -39,7 +59,9 @@ export default defineConfig({
|
|
|
39
59
|
- **Open-in-editor plugin** -- adds dev server middleware for click-to-open
|
|
40
60
|
- **Automatic `es2022` target** -- required for top-level await
|
|
41
61
|
|
|
42
|
-
###
|
|
62
|
+
### 3. Add the debug console (optional, recommended)
|
|
63
|
+
|
|
64
|
+
An in-browser debug console (powered by Eruda) with a dedicated GG tab for filtering and inspecting logs — especially useful on mobile.
|
|
43
65
|
|
|
44
66
|
```svelte
|
|
45
67
|
<!-- src/routes/+layout.svelte -->
|
|
@@ -51,23 +73,7 @@ export default defineConfig({
|
|
|
51
73
|
{@render children()}
|
|
52
74
|
```
|
|
53
75
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
```svelte
|
|
57
|
-
<script>
|
|
58
|
-
import { gg } from '@leftium/gg';
|
|
59
|
-
|
|
60
|
-
gg('Hello world');
|
|
61
|
-
|
|
62
|
-
// Log expressions (returns first argument)
|
|
63
|
-
const result = gg(someFunction());
|
|
64
|
-
|
|
65
|
-
// Multiple arguments
|
|
66
|
-
gg('User:', user, 'Status:', status);
|
|
67
|
-
</script>
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
That's it! In development, a debug console appears automatically.
|
|
76
|
+
In development, the debug console appears automatically.
|
|
71
77
|
In production, add `?gg` to the URL or use a 5-tap gesture to activate.
|
|
72
78
|
|
|
73
79
|
## GgConsole Options
|
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { dev } from '$app/environment';
|
|
3
3
|
|
|
4
|
+
type GgCallSiteInfo = { fileName: string; functionName: string; url: string };
|
|
5
|
+
|
|
4
6
|
let {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
gg,
|
|
8
|
+
url = gg?.url,
|
|
9
|
+
fileName = gg?.fileName,
|
|
10
|
+
title = gg ? `${gg.fileName}@${gg.functionName}` : fileName
|
|
11
|
+
}: {
|
|
12
|
+
gg?: GgCallSiteInfo;
|
|
13
|
+
url?: string;
|
|
14
|
+
fileName?: string;
|
|
15
|
+
title?: string;
|
|
16
|
+
} = $props();
|
|
9
17
|
|
|
10
18
|
// svelte-ignore non_reactive_update
|
|
11
19
|
let iframeElement: HTMLIFrameElement;
|
|
12
20
|
|
|
13
21
|
function onclick(event: MouseEvent) {
|
|
14
|
-
|
|
15
|
-
|
|
22
|
+
if (url) {
|
|
23
|
+
iframeElement.src = url;
|
|
24
|
+
event.preventDefault();
|
|
25
|
+
}
|
|
16
26
|
}
|
|
17
27
|
</script>
|
|
18
28
|
|
|
19
|
-
{#if dev}
|
|
29
|
+
{#if dev && fileName}
|
|
20
30
|
[📝<a {onclick} href={url} {title} target="_open-in-editor" class="open-in-editor-link">
|
|
21
31
|
{fileName}
|
|
22
32
|
</a>
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
type
|
|
2
|
-
url: string;
|
|
1
|
+
type GgCallSiteInfo = {
|
|
3
2
|
fileName: string;
|
|
3
|
+
functionName: string;
|
|
4
|
+
url: string;
|
|
5
|
+
};
|
|
6
|
+
type $$ComponentProps = {
|
|
7
|
+
gg?: GgCallSiteInfo;
|
|
8
|
+
url?: string;
|
|
9
|
+
fileName?: string;
|
|
4
10
|
title?: string;
|
|
5
11
|
};
|
|
6
12
|
declare const OpenInEditorLink: import("svelte").Component<$$ComponentProps, {}, "">;
|
package/dist/eruda/plugin.js
CHANGED
|
@@ -521,7 +521,62 @@ export function createGgPlugin(options, gg) {
|
|
|
521
521
|
padding-bottom: 4px;
|
|
522
522
|
border-bottom: 1px solid #ddd;
|
|
523
523
|
}
|
|
524
|
-
|
|
524
|
+
/* Level-based styling for info/warn/error entries */
|
|
525
|
+
.gg-level-info .gg-log-diff,
|
|
526
|
+
.gg-level-info .gg-log-ns,
|
|
527
|
+
.gg-level-info .gg-log-content {
|
|
528
|
+
background: rgba(23, 162, 184, 0.08);
|
|
529
|
+
}
|
|
530
|
+
.gg-level-info .gg-log-content {
|
|
531
|
+
border-left: 3px solid #17a2b8;
|
|
532
|
+
padding-left: 6px;
|
|
533
|
+
}
|
|
534
|
+
.gg-level-warn .gg-log-diff,
|
|
535
|
+
.gg-level-warn .gg-log-ns,
|
|
536
|
+
.gg-level-warn .gg-log-content {
|
|
537
|
+
background: rgba(255, 200, 0, 0.08);
|
|
538
|
+
}
|
|
539
|
+
.gg-level-warn .gg-log-content {
|
|
540
|
+
border-left: 3px solid #e6a700;
|
|
541
|
+
padding-left: 6px;
|
|
542
|
+
}
|
|
543
|
+
.gg-level-error .gg-log-diff,
|
|
544
|
+
.gg-level-error .gg-log-ns,
|
|
545
|
+
.gg-level-error .gg-log-content {
|
|
546
|
+
background: rgba(255, 50, 50, 0.08);
|
|
547
|
+
}
|
|
548
|
+
.gg-level-error .gg-log-content {
|
|
549
|
+
border-left: 3px solid #cc0000;
|
|
550
|
+
padding-left: 6px;
|
|
551
|
+
}
|
|
552
|
+
/* Stack trace toggle */
|
|
553
|
+
.gg-stack-toggle {
|
|
554
|
+
cursor: pointer;
|
|
555
|
+
font-size: 11px;
|
|
556
|
+
opacity: 0.6;
|
|
557
|
+
margin-left: 8px;
|
|
558
|
+
user-select: none;
|
|
559
|
+
}
|
|
560
|
+
.gg-stack-toggle:hover {
|
|
561
|
+
opacity: 1;
|
|
562
|
+
}
|
|
563
|
+
.gg-stack-content {
|
|
564
|
+
display: none;
|
|
565
|
+
font-size: 11px;
|
|
566
|
+
font-family: monospace;
|
|
567
|
+
white-space: pre;
|
|
568
|
+
padding: 6px 8px;
|
|
569
|
+
margin-top: 4px;
|
|
570
|
+
background: #f0f0f0;
|
|
571
|
+
border-radius: 3px;
|
|
572
|
+
overflow-x: auto;
|
|
573
|
+
color: #666;
|
|
574
|
+
line-height: 1.4;
|
|
575
|
+
}
|
|
576
|
+
.gg-stack-content.expanded {
|
|
577
|
+
display: block;
|
|
578
|
+
}
|
|
579
|
+
.gg-filter-panel {
|
|
525
580
|
background: #f5f5f5;
|
|
526
581
|
padding: 10px;
|
|
527
582
|
margin-bottom: 8px;
|
|
@@ -1214,6 +1269,19 @@ export function createGgPlugin(options, gg) {
|
|
|
1214
1269
|
}
|
|
1215
1270
|
return;
|
|
1216
1271
|
}
|
|
1272
|
+
// Handle stack trace toggle
|
|
1273
|
+
if (target?.classList?.contains('gg-stack-toggle')) {
|
|
1274
|
+
const stackId = target.getAttribute('data-stack-id');
|
|
1275
|
+
if (!stackId)
|
|
1276
|
+
return;
|
|
1277
|
+
const stackEl = containerEl.querySelector(`.gg-stack-content[data-stack-id="${stackId}"]`);
|
|
1278
|
+
if (stackEl) {
|
|
1279
|
+
const isExpanded = stackEl.classList.contains('expanded');
|
|
1280
|
+
stackEl.classList.toggle('expanded');
|
|
1281
|
+
target.textContent = isExpanded ? '▶ stack' : '▼ stack';
|
|
1282
|
+
}
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1217
1285
|
// Handle clicking namespace to open in editor (when filter collapsed)
|
|
1218
1286
|
if (target?.classList?.contains('gg-log-ns') &&
|
|
1219
1287
|
target.hasAttribute('data-file') &&
|
|
@@ -1404,7 +1472,27 @@ export function createGgPlugin(options, gg) {
|
|
|
1404
1472
|
let detailsHTML = '';
|
|
1405
1473
|
// Source expression for this entry (used in hover tooltips and expanded details)
|
|
1406
1474
|
const srcExpr = entry.src?.trim() && !/^['"`]/.test(entry.src) ? escapeHtml(entry.src) : '';
|
|
1407
|
-
|
|
1475
|
+
// HTML table rendering for gg.table() entries
|
|
1476
|
+
if (entry.tableData && entry.tableData.keys.length > 0) {
|
|
1477
|
+
const { keys, rows: tableRows } = entry.tableData;
|
|
1478
|
+
const headerCells = keys
|
|
1479
|
+
.map((k) => `<th style="padding: 2px 8px; border: 1px solid #ccc; background: #f0f0f0; font-size: 11px; white-space: nowrap;">${escapeHtml(k)}</th>`)
|
|
1480
|
+
.join('');
|
|
1481
|
+
const bodyRowsHtml = tableRows
|
|
1482
|
+
.map((row) => {
|
|
1483
|
+
const cells = keys
|
|
1484
|
+
.map((k) => {
|
|
1485
|
+
const val = row[k];
|
|
1486
|
+
const display = val === undefined ? '' : escapeHtml(String(val));
|
|
1487
|
+
return `<td style="padding: 2px 8px; border: 1px solid #ddd; font-size: 11px; white-space: nowrap;">${display}</td>`;
|
|
1488
|
+
})
|
|
1489
|
+
.join('');
|
|
1490
|
+
return `<tr>${cells}</tr>`;
|
|
1491
|
+
})
|
|
1492
|
+
.join('');
|
|
1493
|
+
argsHTML = `<table style="border-collapse: collapse; margin: 2px 0; font-family: monospace;"><thead><tr>${headerCells}</tr></thead><tbody>${bodyRowsHtml}</tbody></table>`;
|
|
1494
|
+
}
|
|
1495
|
+
else if (entry.args.length > 0) {
|
|
1408
1496
|
argsHTML = entry.args
|
|
1409
1497
|
.map((arg, argIdx) => {
|
|
1410
1498
|
if (typeof arg === 'object' && arg !== null) {
|
|
@@ -1460,15 +1548,31 @@ export function createGgPlugin(options, gg) {
|
|
|
1460
1548
|
}
|
|
1461
1549
|
}
|
|
1462
1550
|
const fileTitle = fileTitleText ? ` title="${escapeHtml(fileTitleText)}"` : '';
|
|
1551
|
+
// Level class for info/warn/error styling
|
|
1552
|
+
const levelClass = entry.level === 'info'
|
|
1553
|
+
? ' gg-level-info'
|
|
1554
|
+
: entry.level === 'warn'
|
|
1555
|
+
? ' gg-level-warn'
|
|
1556
|
+
: entry.level === 'error'
|
|
1557
|
+
? ' gg-level-error'
|
|
1558
|
+
: '';
|
|
1559
|
+
// Stack trace toggle (for error/trace entries with captured stacks)
|
|
1560
|
+
let stackHTML = '';
|
|
1561
|
+
if (entry.stack) {
|
|
1562
|
+
const stackId = `stack-${index}`;
|
|
1563
|
+
stackHTML =
|
|
1564
|
+
`<span class="gg-stack-toggle" data-stack-id="${stackId}">▶ stack</span>` +
|
|
1565
|
+
`<div class="gg-stack-content" data-stack-id="${stackId}">${escapeHtml(entry.stack)}</div>`;
|
|
1566
|
+
}
|
|
1463
1567
|
// Desktop: grid layout, Mobile: stacked layout
|
|
1464
|
-
return (`<div class="gg-log-entry">` +
|
|
1568
|
+
return (`<div class="gg-log-entry${levelClass}">` +
|
|
1465
1569
|
`<div class="gg-log-header">` +
|
|
1466
1570
|
iconsCol +
|
|
1467
1571
|
`<div class="gg-log-diff${soloClass}" style="color: ${color};"${soloAttr}>${diff}</div>` +
|
|
1468
1572
|
`<div class="gg-log-ns${soloClass}" style="color: ${color};"${soloAttr}${fileAttr}${lineAttr}${colAttr}${fileTitle}>${ns}</div>` +
|
|
1469
1573
|
`<div class="gg-log-handle"></div>` +
|
|
1470
1574
|
`</div>` +
|
|
1471
|
-
`<div class="gg-log-content"${entry.src?.trim() && !/^['"`]/.test(entry.src) ? ` data-src="${escapeHtml(entry.src)}"` : ''}>${argsHTML}</div>` +
|
|
1575
|
+
`<div class="gg-log-content"${!entry.level && entry.src?.trim() && !/^['"`]/.test(entry.src) ? ` data-src="${escapeHtml(entry.src)}"` : ''}>${argsHTML}${stackHTML}</div>` +
|
|
1472
1576
|
detailsHTML +
|
|
1473
1577
|
`</div>`);
|
|
1474
1578
|
})
|
package/dist/eruda/types.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export interface GgErudaOptions {
|
|
|
18
18
|
*/
|
|
19
19
|
erudaOptions?: Record<string, unknown>;
|
|
20
20
|
}
|
|
21
|
+
/** Log severity level */
|
|
22
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
21
23
|
/**
|
|
22
24
|
* A captured log entry from gg()
|
|
23
25
|
*/
|
|
@@ -42,6 +44,15 @@ export interface CapturedEntry {
|
|
|
42
44
|
col?: number;
|
|
43
45
|
/** Source expression text for icecream-style display (e.g., "user.name") */
|
|
44
46
|
src?: string;
|
|
47
|
+
/** Log severity level (default: 'debug') */
|
|
48
|
+
level?: LogLevel;
|
|
49
|
+
/** Stack trace string (captured for error/trace calls) */
|
|
50
|
+
stack?: string;
|
|
51
|
+
/** Structured table data for gg.table() — Eruda renders as HTML table */
|
|
52
|
+
tableData?: {
|
|
53
|
+
keys: string[];
|
|
54
|
+
rows: Array<Record<string, unknown>>;
|
|
55
|
+
};
|
|
45
56
|
}
|
|
46
57
|
/**
|
|
47
58
|
* Eruda plugin interface
|
|
@@ -39,7 +39,17 @@ export default function ggCallSitesPlugin(options = {}) {
|
|
|
39
39
|
if (!/\.(js|ts|svelte|jsx|tsx|mjs|mts)(\?.*)?$/.test(id))
|
|
40
40
|
return null;
|
|
41
41
|
// Quick bail: no gg calls in this file
|
|
42
|
-
if (!code.includes('gg(') &&
|
|
42
|
+
if (!code.includes('gg(') &&
|
|
43
|
+
!code.includes('gg.ns(') &&
|
|
44
|
+
!code.includes('gg.info(') &&
|
|
45
|
+
!code.includes('gg.warn(') &&
|
|
46
|
+
!code.includes('gg.error(') &&
|
|
47
|
+
!code.includes('gg.table(') &&
|
|
48
|
+
!code.includes('gg.trace(') &&
|
|
49
|
+
!code.includes('gg.assert(') &&
|
|
50
|
+
!code.includes('gg.time(') &&
|
|
51
|
+
!code.includes('gg.timeLog(') &&
|
|
52
|
+
!code.includes('gg.timeEnd('))
|
|
43
53
|
return null;
|
|
44
54
|
// Don't transform gg's own source files
|
|
45
55
|
if (id.includes('/lib/gg.') || id.includes('/lib/debug'))
|
|
@@ -122,7 +132,7 @@ export function collectCodeRanges(code) {
|
|
|
122
132
|
const functionScopes = [];
|
|
123
133
|
// Script blocks (instance + module)
|
|
124
134
|
// The Svelte AST Program node has start/end at runtime but TypeScript's
|
|
125
|
-
// estree Program type doesn't declare them —
|
|
135
|
+
// estree Program type doesn't declare them — we know they exist.
|
|
126
136
|
if (ast.instance) {
|
|
127
137
|
const content = ast.instance.content;
|
|
128
138
|
ranges.push({ start: content.start, end: content.end, context: 'script' });
|
|
@@ -643,61 +653,68 @@ export function transformGgCalls(code, shortPath, filePath, svelteInfo, jsFuncti
|
|
|
643
653
|
// States for string/comment tracking
|
|
644
654
|
let i = 0;
|
|
645
655
|
while (i < code.length) {
|
|
646
|
-
//
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
i
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
656
|
+
// For .svelte files, only apply JS string/comment/backtick skipping inside
|
|
657
|
+
// code ranges (script blocks + template expressions). Outside code ranges,
|
|
658
|
+
// characters like ' " ` // /* are just HTML prose — NOT JS syntax.
|
|
659
|
+
// e.g. "Eruda's" contains an apostrophe that is NOT a JS string delimiter.
|
|
660
|
+
const inCodeRange = !svelteInfo || !!rangeAt(i);
|
|
661
|
+
if (inCodeRange) {
|
|
662
|
+
// Skip single-line comments
|
|
663
|
+
if (code[i] === '/' && code[i + 1] === '/') {
|
|
664
|
+
const end = code.indexOf('\n', i);
|
|
665
|
+
i = end === -1 ? code.length : end + 1;
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
// Skip multi-line comments
|
|
669
|
+
if (code[i] === '/' && code[i + 1] === '*') {
|
|
670
|
+
const end = code.indexOf('*/', i + 2);
|
|
671
|
+
i = end === -1 ? code.length : end + 2;
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
// Skip template literals (backticks)
|
|
675
|
+
if (code[i] === '`') {
|
|
676
|
+
i++;
|
|
677
|
+
let depth = 0;
|
|
678
|
+
while (i < code.length) {
|
|
679
|
+
if (code[i] === '\\') {
|
|
680
|
+
i += 2;
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
if (code[i] === '$' && code[i + 1] === '{') {
|
|
684
|
+
depth++;
|
|
685
|
+
i += 2;
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
if (code[i] === '}' && depth > 0) {
|
|
689
|
+
depth--;
|
|
690
|
+
i++;
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
if (code[i] === '`' && depth === 0) {
|
|
694
|
+
i++;
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
678
697
|
i++;
|
|
679
|
-
break;
|
|
680
698
|
}
|
|
681
|
-
|
|
699
|
+
continue;
|
|
682
700
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
701
|
+
// Skip strings (single and double quotes)
|
|
702
|
+
if (code[i] === '"' || code[i] === "'") {
|
|
703
|
+
const quote = code[i];
|
|
704
|
+
i++;
|
|
705
|
+
while (i < code.length) {
|
|
706
|
+
if (code[i] === '\\') {
|
|
707
|
+
i += 2;
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
if (code[i] === quote) {
|
|
711
|
+
i++;
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
695
714
|
i++;
|
|
696
|
-
break;
|
|
697
715
|
}
|
|
698
|
-
|
|
716
|
+
continue;
|
|
699
717
|
}
|
|
700
|
-
continue;
|
|
701
718
|
}
|
|
702
719
|
// Look for 'gg' pattern — could be gg( or gg.ns(
|
|
703
720
|
if (code[i] === 'g' && code[i + 1] === 'g') {
|
|
@@ -783,7 +800,44 @@ export function transformGgCalls(code, shortPath, filePath, svelteInfo, jsFuncti
|
|
|
783
800
|
i += 6;
|
|
784
801
|
continue;
|
|
785
802
|
}
|
|
786
|
-
//
|
|
803
|
+
// Case 1b: gg.info/warn/error/table/trace/assert → gg._info/_warn/_error/_table/_trace/_assert
|
|
804
|
+
// These methods are rewritten like bare gg() but with their internal variant.
|
|
805
|
+
const dotMethodMatch = code
|
|
806
|
+
.slice(i + 2)
|
|
807
|
+
.match(/^\.(info|warn|error|table|trace|assert|time|timeLog|timeEnd)\(/);
|
|
808
|
+
if (dotMethodMatch) {
|
|
809
|
+
const methodName = dotMethodMatch[1];
|
|
810
|
+
const internalName = `_${methodName}`;
|
|
811
|
+
const methodCallLen = 2 + 1 + methodName.length + 1; // 'gg' + '.' + method + '('
|
|
812
|
+
const openParenPos = i + methodCallLen - 1;
|
|
813
|
+
const { line, col } = getLineCol(code, i);
|
|
814
|
+
const fnName = getFunctionName(i, range);
|
|
815
|
+
const callpoint = `${shortPath}${fnName ? `@${fnName}` : ''}`;
|
|
816
|
+
const escapedNs = escapeForString(callpoint);
|
|
817
|
+
const closeParenPos = findMatchingParen(code, openParenPos);
|
|
818
|
+
if (closeParenPos === -1) {
|
|
819
|
+
i += methodCallLen;
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
const argsText = code.slice(openParenPos + 1, closeParenPos).trim();
|
|
823
|
+
result.push(code.slice(lastIndex, i));
|
|
824
|
+
if (argsText === '') {
|
|
825
|
+
// gg.warn() → gg._warn(opts)
|
|
826
|
+
result.push(`gg.${internalName}(${buildOptions(range, escapedNs, line, col)})`);
|
|
827
|
+
lastIndex = closeParenPos + 1;
|
|
828
|
+
i = closeParenPos + 1;
|
|
829
|
+
}
|
|
830
|
+
else {
|
|
831
|
+
// gg.warn(expr) → gg._warn(opts, expr)
|
|
832
|
+
const escapedSrc = escapeForString(argsText);
|
|
833
|
+
result.push(`gg.${internalName}(${buildOptions(range, escapedNs, line, col, escapedSrc)}, `);
|
|
834
|
+
lastIndex = openParenPos + 1; // keep original args
|
|
835
|
+
i = openParenPos + 1;
|
|
836
|
+
}
|
|
837
|
+
modified = true;
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
// Skip other gg.* calls (gg.enable, gg.disable, gg._ns, gg._onLog, gg.time, etc.)
|
|
787
841
|
if (code[i + 2] === '.') {
|
|
788
842
|
i += 3;
|
|
789
843
|
continue;
|
package/dist/gg.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Hook for capturing gg() output (used by Eruda plugin)
|
|
3
3
|
*/
|
|
4
|
+
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
4
5
|
interface CapturedEntry {
|
|
5
6
|
namespace: string;
|
|
6
7
|
color: string;
|
|
@@ -12,6 +13,12 @@ interface CapturedEntry {
|
|
|
12
13
|
line?: number;
|
|
13
14
|
col?: number;
|
|
14
15
|
src?: string;
|
|
16
|
+
level?: LogLevel;
|
|
17
|
+
stack?: string;
|
|
18
|
+
tableData?: {
|
|
19
|
+
keys: string[];
|
|
20
|
+
rows: Array<Record<string, unknown>>;
|
|
21
|
+
};
|
|
15
22
|
}
|
|
16
23
|
type OnLogCallback = (entry: CapturedEntry) => void;
|
|
17
24
|
export declare function gg(): {
|
|
@@ -93,6 +100,8 @@ export declare namespace gg {
|
|
|
93
100
|
line?: number;
|
|
94
101
|
col?: number;
|
|
95
102
|
src?: string;
|
|
103
|
+
level?: LogLevel;
|
|
104
|
+
stack?: string;
|
|
96
105
|
}, ...args: unknown[]) => unknown;
|
|
97
106
|
let _o: (ns: string, file?: string, line?: number, col?: number, src?: string) => {
|
|
98
107
|
ns: string;
|
|
@@ -101,6 +110,78 @@ export declare namespace gg {
|
|
|
101
110
|
col?: number;
|
|
102
111
|
src?: string;
|
|
103
112
|
};
|
|
113
|
+
let info: (...args: unknown[]) => unknown;
|
|
114
|
+
let warn: (...args: unknown[]) => unknown;
|
|
115
|
+
let error: (...args: unknown[]) => unknown;
|
|
116
|
+
let assert: (condition: unknown, ...args: unknown[]) => unknown;
|
|
117
|
+
let table: (data: unknown, columns?: string[]) => unknown;
|
|
118
|
+
let time: (label?: string) => void;
|
|
119
|
+
let timeLog: (label?: string, ...args: unknown[]) => void;
|
|
120
|
+
let timeEnd: (label?: string) => void;
|
|
121
|
+
let trace: (...args: unknown[]) => unknown;
|
|
122
|
+
let _info: (options: {
|
|
123
|
+
ns: string;
|
|
124
|
+
file?: string;
|
|
125
|
+
line?: number;
|
|
126
|
+
col?: number;
|
|
127
|
+
src?: string;
|
|
128
|
+
}, ...args: unknown[]) => unknown;
|
|
129
|
+
let _warn: (options: {
|
|
130
|
+
ns: string;
|
|
131
|
+
file?: string;
|
|
132
|
+
line?: number;
|
|
133
|
+
col?: number;
|
|
134
|
+
src?: string;
|
|
135
|
+
}, ...args: unknown[]) => unknown;
|
|
136
|
+
let _error: (options: {
|
|
137
|
+
ns: string;
|
|
138
|
+
file?: string;
|
|
139
|
+
line?: number;
|
|
140
|
+
col?: number;
|
|
141
|
+
src?: string;
|
|
142
|
+
}, ...args: unknown[]) => unknown;
|
|
143
|
+
let _assert: (options: {
|
|
144
|
+
ns: string;
|
|
145
|
+
file?: string;
|
|
146
|
+
line?: number;
|
|
147
|
+
col?: number;
|
|
148
|
+
src?: string;
|
|
149
|
+
}, condition: unknown, ...args: unknown[]) => unknown;
|
|
150
|
+
let _table: (options: {
|
|
151
|
+
ns: string;
|
|
152
|
+
file?: string;
|
|
153
|
+
line?: number;
|
|
154
|
+
col?: number;
|
|
155
|
+
src?: string;
|
|
156
|
+
}, data: unknown, columns?: string[]) => unknown;
|
|
157
|
+
let _trace: (options: {
|
|
158
|
+
ns: string;
|
|
159
|
+
file?: string;
|
|
160
|
+
line?: number;
|
|
161
|
+
col?: number;
|
|
162
|
+
src?: string;
|
|
163
|
+
}, ...args: unknown[]) => unknown;
|
|
164
|
+
let _time: (options: {
|
|
165
|
+
ns: string;
|
|
166
|
+
file?: string;
|
|
167
|
+
line?: number;
|
|
168
|
+
col?: number;
|
|
169
|
+
src?: string;
|
|
170
|
+
}, label?: string) => void;
|
|
171
|
+
let _timeLog: (options: {
|
|
172
|
+
ns: string;
|
|
173
|
+
file?: string;
|
|
174
|
+
line?: number;
|
|
175
|
+
col?: number;
|
|
176
|
+
src?: string;
|
|
177
|
+
}, label?: string, ...args: unknown[]) => void;
|
|
178
|
+
let _timeEnd: (options: {
|
|
179
|
+
ns: string;
|
|
180
|
+
file?: string;
|
|
181
|
+
line?: number;
|
|
182
|
+
col?: number;
|
|
183
|
+
src?: string;
|
|
184
|
+
}, label?: string) => void;
|
|
104
185
|
}
|
|
105
186
|
/**
|
|
106
187
|
* Run gg diagnostics and log configuration status
|
package/dist/gg.js
CHANGED
|
@@ -207,53 +207,7 @@ export function gg(...args) {
|
|
|
207
207
|
// Same call site always produces the same word pair (e.g. "calm-fox").
|
|
208
208
|
// depth=2: skip "Error" header [0] and gg() frame [1]
|
|
209
209
|
const callpoint = resolveCallpoint(2);
|
|
210
|
-
|
|
211
|
-
const ggLogFunction = namespaceToLogFunction.get(namespace) ||
|
|
212
|
-
namespaceToLogFunction.set(namespace, createGgDebugger(namespace)).get(namespace);
|
|
213
|
-
// Prepare args for logging
|
|
214
|
-
let logArgs;
|
|
215
|
-
let returnValue;
|
|
216
|
-
if (!args.length) {
|
|
217
|
-
// No arguments: return stub call-site info (no open-in-editor without plugin)
|
|
218
|
-
logArgs = [` 📝 ${callpoint} (install gg-call-sites-plugin for editor links)`];
|
|
219
|
-
returnValue = {
|
|
220
|
-
fileName: callpoint,
|
|
221
|
-
functionName: '',
|
|
222
|
-
url: ''
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
else if (args.length === 1) {
|
|
226
|
-
logArgs = [args[0]];
|
|
227
|
-
returnValue = args[0];
|
|
228
|
-
}
|
|
229
|
-
else {
|
|
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));
|
|
239
|
-
}
|
|
240
|
-
// Call capture hook if registered (for Eruda plugin)
|
|
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);
|
|
255
|
-
}
|
|
256
|
-
return returnValue;
|
|
210
|
+
return ggLog({ ns: callpoint }, ...args);
|
|
257
211
|
}
|
|
258
212
|
/**
|
|
259
213
|
* gg.ns() - Log with an explicit namespace (callpoint label).
|
|
@@ -291,19 +245,14 @@ gg.ns = function (nsLabel, ...args) {
|
|
|
291
245
|
return gg._ns({ ns: nsLabel }, ...args);
|
|
292
246
|
};
|
|
293
247
|
/**
|
|
294
|
-
*
|
|
248
|
+
* Core logging function shared by all gg methods.
|
|
295
249
|
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
* location for open-in-editor support.
|
|
300
|
-
*
|
|
301
|
-
* @param options - { ns: string; file?: string; line?: number; col?: number }
|
|
302
|
-
* @param args - Same arguments as gg()
|
|
303
|
-
* @returns Same as gg() - the first arg, or call-site info if no args
|
|
250
|
+
* All public methods (gg, gg.ns, gg.warn, gg.error, gg.table, etc.)
|
|
251
|
+
* funnel through this function. It handles namespace resolution,
|
|
252
|
+
* debug output, capture hook, and passthrough return.
|
|
304
253
|
*/
|
|
305
|
-
|
|
306
|
-
const { ns: nsLabel, file, line, col, src } = options;
|
|
254
|
+
function ggLog(options, ...args) {
|
|
255
|
+
const { ns: nsLabel, file, line, col, src, level, stack, tableData } = options;
|
|
307
256
|
if (!ggConfig.enabled || isCloudflareWorker()) {
|
|
308
257
|
return args.length ? args[0] : { fileName: '', functionName: '', url: '' };
|
|
309
258
|
}
|
|
@@ -333,6 +282,16 @@ gg._ns = function (options, ...args) {
|
|
|
333
282
|
logArgs = [args[0], ...args.slice(1)];
|
|
334
283
|
returnValue = args[0];
|
|
335
284
|
}
|
|
285
|
+
// Add level prefix emoji for info/warn/error
|
|
286
|
+
if (level === 'info') {
|
|
287
|
+
logArgs[0] = `ℹ️ ${logArgs[0]}`;
|
|
288
|
+
}
|
|
289
|
+
else if (level === 'warn') {
|
|
290
|
+
logArgs[0] = `⚠️ ${logArgs[0]}`;
|
|
291
|
+
}
|
|
292
|
+
else if (level === 'error') {
|
|
293
|
+
logArgs[0] = `⛔ ${logArgs[0]}`;
|
|
294
|
+
}
|
|
336
295
|
// Log to console via debug
|
|
337
296
|
if (logArgs.length === 1) {
|
|
338
297
|
ggLogFunction(logArgs[0]);
|
|
@@ -351,7 +310,10 @@ gg._ns = function (options, ...args) {
|
|
|
351
310
|
file,
|
|
352
311
|
line,
|
|
353
312
|
col,
|
|
354
|
-
src
|
|
313
|
+
src,
|
|
314
|
+
level,
|
|
315
|
+
stack,
|
|
316
|
+
tableData
|
|
355
317
|
};
|
|
356
318
|
if (_onLogCallback) {
|
|
357
319
|
_onLogCallback(entry);
|
|
@@ -360,6 +322,21 @@ gg._ns = function (options, ...args) {
|
|
|
360
322
|
earlyLogBuffer.push(entry);
|
|
361
323
|
}
|
|
362
324
|
return returnValue;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* gg._ns() - Internal: log with namespace and source file metadata.
|
|
328
|
+
*
|
|
329
|
+
* Called by the ggCallSitesPlugin Vite plugin, which rewrites both bare gg()
|
|
330
|
+
* calls and manual gg.ns() calls to gg._ns({ns, file, line, col}, ...) at
|
|
331
|
+
* build time. This gives each call site a unique namespace plus the source
|
|
332
|
+
* location for open-in-editor support.
|
|
333
|
+
*
|
|
334
|
+
* @param options - { ns: string; file?: string; line?: number; col?: number }
|
|
335
|
+
* @param args - Same arguments as gg()
|
|
336
|
+
* @returns Same as gg() - the first arg, or call-site info if no args
|
|
337
|
+
*/
|
|
338
|
+
gg._ns = function (options, ...args) {
|
|
339
|
+
return ggLog(options, ...args);
|
|
363
340
|
};
|
|
364
341
|
/**
|
|
365
342
|
* gg._o() - Internal: build options object for gg._ns() without object literal syntax.
|
|
@@ -390,6 +367,380 @@ gg.clearPersist = () => {
|
|
|
390
367
|
}
|
|
391
368
|
}
|
|
392
369
|
};
|
|
370
|
+
// ── Console-like methods ───────────────────────────────────────────────
|
|
371
|
+
// Each public method (gg.warn, gg.error, etc.) has a corresponding internal
|
|
372
|
+
// method (gg._warn, gg._error, etc.) that accepts call-site metadata from
|
|
373
|
+
// the Vite plugin. The public methods use runtime stack-based callpoints
|
|
374
|
+
// as a fallback when the plugin isn't installed.
|
|
375
|
+
/**
|
|
376
|
+
* Capture a cleaned-up stack trace, stripping internal gg frames.
|
|
377
|
+
* @param skipFrames - Number of internal frames to strip from the top
|
|
378
|
+
*/
|
|
379
|
+
function captureStack(skipFrames) {
|
|
380
|
+
let stack = new Error().stack || undefined;
|
|
381
|
+
if (stack) {
|
|
382
|
+
const lines = stack.split('\n');
|
|
383
|
+
stack = lines.slice(skipFrames).join('\n');
|
|
384
|
+
}
|
|
385
|
+
return stack;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Get stack from an Error arg or capture a fresh one.
|
|
389
|
+
*/
|
|
390
|
+
function getErrorStack(firstArg, skipFrames) {
|
|
391
|
+
if (firstArg instanceof Error && firstArg.stack) {
|
|
392
|
+
return firstArg.stack;
|
|
393
|
+
}
|
|
394
|
+
return captureStack(skipFrames);
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* gg.info() - Log at info level.
|
|
398
|
+
*
|
|
399
|
+
* Passthrough: returns the first argument.
|
|
400
|
+
* In Eruda, entries are styled with a blue/info indicator.
|
|
401
|
+
*
|
|
402
|
+
* @example
|
|
403
|
+
* gg.info('System startup complete');
|
|
404
|
+
* const config = gg.info(loadedConfig, 'loaded config');
|
|
405
|
+
*/
|
|
406
|
+
gg.info = function (...args) {
|
|
407
|
+
if (!ggConfig.enabled || isCloudflareWorker()) {
|
|
408
|
+
return args.length ? args[0] : undefined;
|
|
409
|
+
}
|
|
410
|
+
const callpoint = resolveCallpoint(3);
|
|
411
|
+
return ggLog({ ns: callpoint, level: 'info' }, ...args);
|
|
412
|
+
};
|
|
413
|
+
/**
|
|
414
|
+
* gg._info() - Internal: info with call-site metadata from Vite plugin.
|
|
415
|
+
*/
|
|
416
|
+
gg._info = function (options, ...args) {
|
|
417
|
+
return ggLog({ ...options, level: 'info' }, ...args);
|
|
418
|
+
};
|
|
419
|
+
/**
|
|
420
|
+
* gg.warn() - Log at warning level.
|
|
421
|
+
*
|
|
422
|
+
* Passthrough: returns the first argument.
|
|
423
|
+
* In Eruda, entries are styled with a yellow/warning indicator.
|
|
424
|
+
*
|
|
425
|
+
* @example
|
|
426
|
+
* gg.warn('deprecated API used');
|
|
427
|
+
* const result = gg.warn(computeValue(), 'might be slow');
|
|
428
|
+
*/
|
|
429
|
+
gg.warn = function (...args) {
|
|
430
|
+
if (!ggConfig.enabled || isCloudflareWorker()) {
|
|
431
|
+
return args.length ? args[0] : undefined;
|
|
432
|
+
}
|
|
433
|
+
const callpoint = resolveCallpoint(3);
|
|
434
|
+
return ggLog({ ns: callpoint, level: 'warn' }, ...args);
|
|
435
|
+
};
|
|
436
|
+
/**
|
|
437
|
+
* gg._warn() - Internal: warn with call-site metadata from Vite plugin.
|
|
438
|
+
*/
|
|
439
|
+
gg._warn = function (options, ...args) {
|
|
440
|
+
return ggLog({ ...options, level: 'warn' }, ...args);
|
|
441
|
+
};
|
|
442
|
+
/**
|
|
443
|
+
* gg.error() - Log at error level.
|
|
444
|
+
*
|
|
445
|
+
* Passthrough: returns the first argument.
|
|
446
|
+
* Captures a stack trace silently — visible in Eruda via a collapsible toggle.
|
|
447
|
+
* If the first argument is an Error object, its .stack is used instead.
|
|
448
|
+
*
|
|
449
|
+
* @example
|
|
450
|
+
* gg.error('connection failed');
|
|
451
|
+
* gg.error(new Error('timeout'));
|
|
452
|
+
* const val = gg.error(response, 'unexpected status');
|
|
453
|
+
*/
|
|
454
|
+
gg.error = function (...args) {
|
|
455
|
+
if (!ggConfig.enabled || isCloudflareWorker()) {
|
|
456
|
+
return args.length ? args[0] : undefined;
|
|
457
|
+
}
|
|
458
|
+
const callpoint = resolveCallpoint(3);
|
|
459
|
+
const stack = getErrorStack(args[0], 4);
|
|
460
|
+
return ggLog({ ns: callpoint, level: 'error', stack }, ...args);
|
|
461
|
+
};
|
|
462
|
+
/**
|
|
463
|
+
* gg._error() - Internal: error with call-site metadata from Vite plugin.
|
|
464
|
+
*/
|
|
465
|
+
gg._error = function (options, ...args) {
|
|
466
|
+
const stack = getErrorStack(args[0], 3);
|
|
467
|
+
return ggLog({ ...options, level: 'error', stack }, ...args);
|
|
468
|
+
};
|
|
469
|
+
/**
|
|
470
|
+
* gg.assert() - Log only if condition is false.
|
|
471
|
+
*
|
|
472
|
+
* Like console.assert: if the first argument is falsy, logs the remaining
|
|
473
|
+
* arguments at error level. If the condition is truthy, does nothing.
|
|
474
|
+
* Passthrough: always returns the condition value.
|
|
475
|
+
*
|
|
476
|
+
* @example
|
|
477
|
+
* gg.assert(user != null, 'user should exist');
|
|
478
|
+
* gg.assert(list.length > 0, 'list is empty', list);
|
|
479
|
+
*/
|
|
480
|
+
gg.assert = function (condition, ...args) {
|
|
481
|
+
if (!condition) {
|
|
482
|
+
if (!ggConfig.enabled || isCloudflareWorker())
|
|
483
|
+
return condition;
|
|
484
|
+
const callpoint = resolveCallpoint(3);
|
|
485
|
+
const stack = captureStack(4);
|
|
486
|
+
const assertArgs = args.length > 0 ? args : ['Assertion failed'];
|
|
487
|
+
ggLog({ ns: callpoint, level: 'error', stack }, ...assertArgs);
|
|
488
|
+
}
|
|
489
|
+
return condition;
|
|
490
|
+
};
|
|
491
|
+
/**
|
|
492
|
+
* gg._assert() - Internal: assert with call-site metadata from Vite plugin.
|
|
493
|
+
*/
|
|
494
|
+
gg._assert = function (options, condition, ...args) {
|
|
495
|
+
if (!condition) {
|
|
496
|
+
if (!ggConfig.enabled || isCloudflareWorker())
|
|
497
|
+
return condition;
|
|
498
|
+
const stack = captureStack(3);
|
|
499
|
+
const assertArgs = args.length > 0 ? args : ['Assertion failed'];
|
|
500
|
+
ggLog({ ...options, level: 'error', stack }, ...assertArgs);
|
|
501
|
+
}
|
|
502
|
+
return condition;
|
|
503
|
+
};
|
|
504
|
+
/**
|
|
505
|
+
* gg.table() - Log tabular data.
|
|
506
|
+
*
|
|
507
|
+
* Formats an array of objects (or an object of objects) as an ASCII table.
|
|
508
|
+
* Passthrough: returns the data argument.
|
|
509
|
+
*
|
|
510
|
+
* @example
|
|
511
|
+
* gg.table([{name: 'Alice', age: 30}, {name: 'Bob', age: 25}]);
|
|
512
|
+
* gg.table({a: {x: 1}, b: {x: 2}});
|
|
513
|
+
*/
|
|
514
|
+
gg.table = function (data, columns) {
|
|
515
|
+
if (!ggConfig.enabled || isCloudflareWorker())
|
|
516
|
+
return data;
|
|
517
|
+
const callpoint = resolveCallpoint(3);
|
|
518
|
+
const { keys, rows } = formatTable(data, columns);
|
|
519
|
+
ggLog({ ns: callpoint, tableData: { keys, rows } }, '(table)');
|
|
520
|
+
// Also emit a native console.table for proper rendering in browser/Node consoles
|
|
521
|
+
if (columns) {
|
|
522
|
+
console.table(data, columns);
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
console.table(data);
|
|
526
|
+
}
|
|
527
|
+
return data;
|
|
528
|
+
};
|
|
529
|
+
/**
|
|
530
|
+
* gg._table() - Internal: table with call-site metadata from Vite plugin.
|
|
531
|
+
*/
|
|
532
|
+
gg._table = function (options, data, columns) {
|
|
533
|
+
if (!ggConfig.enabled || isCloudflareWorker())
|
|
534
|
+
return data;
|
|
535
|
+
const { keys, rows } = formatTable(data, columns);
|
|
536
|
+
ggLog({ ...options, tableData: { keys, rows } }, '(table)');
|
|
537
|
+
if (columns) {
|
|
538
|
+
console.table(data, columns);
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
console.table(data);
|
|
542
|
+
}
|
|
543
|
+
return data;
|
|
544
|
+
};
|
|
545
|
+
// Timer storage for gg.time / gg.timeEnd / gg.timeLog
|
|
546
|
+
const timers = new Map();
|
|
547
|
+
/**
|
|
548
|
+
* gg.time() - Start a named timer.
|
|
549
|
+
*
|
|
550
|
+
* @example
|
|
551
|
+
* gg.time('fetch');
|
|
552
|
+
* const data = await fetchData();
|
|
553
|
+
* gg.timeEnd('fetch'); // logs "+123ms fetch: 456ms"
|
|
554
|
+
*/
|
|
555
|
+
gg.time = function (label = 'default') {
|
|
556
|
+
if (!ggConfig.enabled || isCloudflareWorker())
|
|
557
|
+
return;
|
|
558
|
+
timers.set(label, performance.now());
|
|
559
|
+
};
|
|
560
|
+
/** gg._time() - Internal: time with call-site metadata from Vite plugin. */
|
|
561
|
+
gg._time = function (_options, label = 'default') {
|
|
562
|
+
if (!ggConfig.enabled || isCloudflareWorker())
|
|
563
|
+
return;
|
|
564
|
+
timers.set(label, performance.now());
|
|
565
|
+
};
|
|
566
|
+
/**
|
|
567
|
+
* gg.timeLog() - Log the current elapsed time without stopping the timer.
|
|
568
|
+
*
|
|
569
|
+
* @example
|
|
570
|
+
* gg.time('process');
|
|
571
|
+
* // ... step 1 ...
|
|
572
|
+
* gg.timeLog('process', 'step 1 done');
|
|
573
|
+
* // ... step 2 ...
|
|
574
|
+
* gg.timeEnd('process');
|
|
575
|
+
*/
|
|
576
|
+
gg.timeLog = function (label = 'default', ...args) {
|
|
577
|
+
if (!ggConfig.enabled || isCloudflareWorker())
|
|
578
|
+
return;
|
|
579
|
+
const start = timers.get(label);
|
|
580
|
+
if (start === undefined) {
|
|
581
|
+
const callpoint = resolveCallpoint(3);
|
|
582
|
+
ggLog({ ns: callpoint, level: 'warn' }, `Timer '${label}' does not exist`);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
const elapsed = performance.now() - start;
|
|
586
|
+
const callpoint = resolveCallpoint(3);
|
|
587
|
+
ggLog({ ns: callpoint }, `${label}: ${formatElapsed(elapsed)}`, ...args);
|
|
588
|
+
};
|
|
589
|
+
/** gg._timeLog() - Internal: timeLog with call-site metadata from Vite plugin. */
|
|
590
|
+
gg._timeLog = function (options, label = 'default', ...args) {
|
|
591
|
+
if (!ggConfig.enabled || isCloudflareWorker())
|
|
592
|
+
return;
|
|
593
|
+
const start = timers.get(label);
|
|
594
|
+
if (start === undefined) {
|
|
595
|
+
ggLog({ ...options, level: 'warn' }, `Timer '${label}' does not exist`);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
const elapsed = performance.now() - start;
|
|
599
|
+
ggLog(options, `${label}: ${formatElapsed(elapsed)}`, ...args);
|
|
600
|
+
};
|
|
601
|
+
/**
|
|
602
|
+
* gg.timeEnd() - Stop a named timer and log the elapsed time.
|
|
603
|
+
*
|
|
604
|
+
* @example
|
|
605
|
+
* gg.time('fetch');
|
|
606
|
+
* const data = await fetchData();
|
|
607
|
+
* gg.timeEnd('fetch'); // logs "fetch: 456.12ms"
|
|
608
|
+
*/
|
|
609
|
+
gg.timeEnd = function (label = 'default') {
|
|
610
|
+
if (!ggConfig.enabled || isCloudflareWorker())
|
|
611
|
+
return;
|
|
612
|
+
const start = timers.get(label);
|
|
613
|
+
if (start === undefined) {
|
|
614
|
+
const callpoint = resolveCallpoint(3);
|
|
615
|
+
ggLog({ ns: callpoint, level: 'warn' }, `Timer '${label}' does not exist`);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
const elapsed = performance.now() - start;
|
|
619
|
+
timers.delete(label);
|
|
620
|
+
const callpoint = resolveCallpoint(3);
|
|
621
|
+
ggLog({ ns: callpoint }, `${label}: ${formatElapsed(elapsed)}`);
|
|
622
|
+
};
|
|
623
|
+
/** gg._timeEnd() - Internal: timeEnd with call-site metadata from Vite plugin. */
|
|
624
|
+
gg._timeEnd = function (options, label = 'default') {
|
|
625
|
+
if (!ggConfig.enabled || isCloudflareWorker())
|
|
626
|
+
return;
|
|
627
|
+
const start = timers.get(label);
|
|
628
|
+
if (start === undefined) {
|
|
629
|
+
ggLog({ ...options, level: 'warn' }, `Timer '${label}' does not exist`);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
const elapsed = performance.now() - start;
|
|
633
|
+
timers.delete(label);
|
|
634
|
+
ggLog(options, `${label}: ${formatElapsed(elapsed)}`);
|
|
635
|
+
};
|
|
636
|
+
/**
|
|
637
|
+
* gg.trace() - Log with a stack trace.
|
|
638
|
+
*
|
|
639
|
+
* Like console.trace: logs the arguments plus a full stack trace.
|
|
640
|
+
* Passthrough: returns the first argument.
|
|
641
|
+
*
|
|
642
|
+
* @example
|
|
643
|
+
* gg.trace('how did we get here?');
|
|
644
|
+
* const val = gg.trace(result, 'call path');
|
|
645
|
+
*/
|
|
646
|
+
gg.trace = function (...args) {
|
|
647
|
+
if (!ggConfig.enabled || isCloudflareWorker()) {
|
|
648
|
+
return args.length ? args[0] : undefined;
|
|
649
|
+
}
|
|
650
|
+
const callpoint = resolveCallpoint(3);
|
|
651
|
+
const stack = captureStack(4);
|
|
652
|
+
const traceArgs = args.length > 0 ? args : ['Trace'];
|
|
653
|
+
return ggLog({ ns: callpoint, stack }, ...traceArgs);
|
|
654
|
+
};
|
|
655
|
+
/**
|
|
656
|
+
* gg._trace() - Internal: trace with call-site metadata from Vite plugin.
|
|
657
|
+
*/
|
|
658
|
+
gg._trace = function (options, ...args) {
|
|
659
|
+
if (!ggConfig.enabled || isCloudflareWorker()) {
|
|
660
|
+
return args.length ? args[0] : undefined;
|
|
661
|
+
}
|
|
662
|
+
const stack = captureStack(3);
|
|
663
|
+
const traceArgs = args.length > 0 ? args : ['Trace'];
|
|
664
|
+
return ggLog({ ...options, stack }, ...traceArgs);
|
|
665
|
+
};
|
|
666
|
+
/**
|
|
667
|
+
* Format elapsed time with appropriate precision.
|
|
668
|
+
* < 1s → "123.45ms", >= 1s → "1.23s", >= 60s → "1m 2.3s"
|
|
669
|
+
*/
|
|
670
|
+
function formatElapsed(ms) {
|
|
671
|
+
if (ms < 1000)
|
|
672
|
+
return `${ms.toFixed(2)}ms`;
|
|
673
|
+
if (ms < 60000)
|
|
674
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
675
|
+
const minutes = Math.floor(ms / 60000);
|
|
676
|
+
const seconds = (ms % 60000) / 1000;
|
|
677
|
+
return `${minutes}m ${seconds.toFixed(1)}s`;
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Normalize data into structured keys + rows for table rendering.
|
|
681
|
+
* Used by both Eruda (HTML table) and console.table() delegation.
|
|
682
|
+
* Supports arrays of objects, arrays of primitives, and objects of objects.
|
|
683
|
+
*/
|
|
684
|
+
function formatTable(data, columns) {
|
|
685
|
+
if (data === null || data === undefined || typeof data !== 'object') {
|
|
686
|
+
return { keys: [], rows: [] };
|
|
687
|
+
}
|
|
688
|
+
// Normalize to rows: [{key, ...values}]
|
|
689
|
+
let rows;
|
|
690
|
+
let allKeys;
|
|
691
|
+
if (Array.isArray(data)) {
|
|
692
|
+
if (data.length === 0)
|
|
693
|
+
return { keys: [], rows: [] };
|
|
694
|
+
// Array of primitives
|
|
695
|
+
if (typeof data[0] !== 'object' || data[0] === null) {
|
|
696
|
+
allKeys = ['(index)', 'Value'];
|
|
697
|
+
rows = data.map((v, i) => ({ '(index)': i, Value: v }));
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
// Array of objects
|
|
701
|
+
const keySet = new Set();
|
|
702
|
+
keySet.add('(index)');
|
|
703
|
+
for (const item of data) {
|
|
704
|
+
if (item && typeof item === 'object') {
|
|
705
|
+
Object.keys(item).forEach((k) => keySet.add(k));
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
allKeys = Array.from(keySet);
|
|
709
|
+
rows = data.map((item, i) => ({
|
|
710
|
+
'(index)': i,
|
|
711
|
+
...(item && typeof item === 'object' ? item : { Value: item })
|
|
712
|
+
}));
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
else {
|
|
716
|
+
// Object of objects/values
|
|
717
|
+
const entries = Object.entries(data);
|
|
718
|
+
if (entries.length === 0)
|
|
719
|
+
return { keys: [], rows: [] };
|
|
720
|
+
const keySet = new Set();
|
|
721
|
+
keySet.add('(index)');
|
|
722
|
+
for (const [, val] of entries) {
|
|
723
|
+
if (val && typeof val === 'object' && !Array.isArray(val)) {
|
|
724
|
+
Object.keys(val).forEach((k) => keySet.add(k));
|
|
725
|
+
}
|
|
726
|
+
else {
|
|
727
|
+
keySet.add('Value');
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
allKeys = Array.from(keySet);
|
|
731
|
+
rows = entries.map(([key, val]) => ({
|
|
732
|
+
'(index)': key,
|
|
733
|
+
...(val && typeof val === 'object' && !Array.isArray(val)
|
|
734
|
+
? val
|
|
735
|
+
: { Value: val })
|
|
736
|
+
}));
|
|
737
|
+
}
|
|
738
|
+
// Apply column filter
|
|
739
|
+
if (columns && columns.length > 0) {
|
|
740
|
+
allKeys = ['(index)', ...columns.filter((c) => allKeys.includes(c))];
|
|
741
|
+
}
|
|
742
|
+
return { keys: allKeys, rows };
|
|
743
|
+
}
|
|
393
744
|
/**
|
|
394
745
|
* Parse color string to RGB values
|
|
395
746
|
* Accepts: named colors, hex (#rgb, #rrggbb), rgb(r,g,b), rgba(r,g,b,a)
|