@leftium/gg 0.0.34 → 0.0.35
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 +97 -4
- package/dist/eruda/types.d.ts +11 -0
- package/dist/gg-call-sites-plugin.js +105 -52
- package/dist/gg.d.ts +73 -0
- package/dist/gg.js +384 -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,53 @@ 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 warn/error entries */
|
|
525
|
+
.gg-level-warn .gg-log-diff,
|
|
526
|
+
.gg-level-warn .gg-log-ns,
|
|
527
|
+
.gg-level-warn .gg-log-content {
|
|
528
|
+
background: rgba(255, 200, 0, 0.08);
|
|
529
|
+
}
|
|
530
|
+
.gg-level-warn .gg-log-content {
|
|
531
|
+
border-left: 3px solid #e6a700;
|
|
532
|
+
padding-left: 6px;
|
|
533
|
+
}
|
|
534
|
+
.gg-level-error .gg-log-diff,
|
|
535
|
+
.gg-level-error .gg-log-ns,
|
|
536
|
+
.gg-level-error .gg-log-content {
|
|
537
|
+
background: rgba(255, 50, 50, 0.08);
|
|
538
|
+
}
|
|
539
|
+
.gg-level-error .gg-log-content {
|
|
540
|
+
border-left: 3px solid #cc0000;
|
|
541
|
+
padding-left: 6px;
|
|
542
|
+
}
|
|
543
|
+
/* Stack trace toggle */
|
|
544
|
+
.gg-stack-toggle {
|
|
545
|
+
cursor: pointer;
|
|
546
|
+
font-size: 11px;
|
|
547
|
+
opacity: 0.6;
|
|
548
|
+
margin-left: 8px;
|
|
549
|
+
user-select: none;
|
|
550
|
+
}
|
|
551
|
+
.gg-stack-toggle:hover {
|
|
552
|
+
opacity: 1;
|
|
553
|
+
}
|
|
554
|
+
.gg-stack-content {
|
|
555
|
+
display: none;
|
|
556
|
+
font-size: 11px;
|
|
557
|
+
font-family: monospace;
|
|
558
|
+
white-space: pre;
|
|
559
|
+
padding: 6px 8px;
|
|
560
|
+
margin-top: 4px;
|
|
561
|
+
background: #f0f0f0;
|
|
562
|
+
border-radius: 3px;
|
|
563
|
+
overflow-x: auto;
|
|
564
|
+
color: #666;
|
|
565
|
+
line-height: 1.4;
|
|
566
|
+
}
|
|
567
|
+
.gg-stack-content.expanded {
|
|
568
|
+
display: block;
|
|
569
|
+
}
|
|
570
|
+
.gg-filter-panel {
|
|
525
571
|
background: #f5f5f5;
|
|
526
572
|
padding: 10px;
|
|
527
573
|
margin-bottom: 8px;
|
|
@@ -1214,6 +1260,19 @@ export function createGgPlugin(options, gg) {
|
|
|
1214
1260
|
}
|
|
1215
1261
|
return;
|
|
1216
1262
|
}
|
|
1263
|
+
// Handle stack trace toggle
|
|
1264
|
+
if (target?.classList?.contains('gg-stack-toggle')) {
|
|
1265
|
+
const stackId = target.getAttribute('data-stack-id');
|
|
1266
|
+
if (!stackId)
|
|
1267
|
+
return;
|
|
1268
|
+
const stackEl = containerEl.querySelector(`.gg-stack-content[data-stack-id="${stackId}"]`);
|
|
1269
|
+
if (stackEl) {
|
|
1270
|
+
const isExpanded = stackEl.classList.contains('expanded');
|
|
1271
|
+
stackEl.classList.toggle('expanded');
|
|
1272
|
+
target.textContent = isExpanded ? '▶ stack' : '▼ stack';
|
|
1273
|
+
}
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1217
1276
|
// Handle clicking namespace to open in editor (when filter collapsed)
|
|
1218
1277
|
if (target?.classList?.contains('gg-log-ns') &&
|
|
1219
1278
|
target.hasAttribute('data-file') &&
|
|
@@ -1404,7 +1463,27 @@ export function createGgPlugin(options, gg) {
|
|
|
1404
1463
|
let detailsHTML = '';
|
|
1405
1464
|
// Source expression for this entry (used in hover tooltips and expanded details)
|
|
1406
1465
|
const srcExpr = entry.src?.trim() && !/^['"`]/.test(entry.src) ? escapeHtml(entry.src) : '';
|
|
1407
|
-
|
|
1466
|
+
// HTML table rendering for gg.table() entries
|
|
1467
|
+
if (entry.tableData && entry.tableData.keys.length > 0) {
|
|
1468
|
+
const { keys, rows: tableRows } = entry.tableData;
|
|
1469
|
+
const headerCells = keys
|
|
1470
|
+
.map((k) => `<th style="padding: 2px 8px; border: 1px solid #ccc; background: #f0f0f0; font-size: 11px; white-space: nowrap;">${escapeHtml(k)}</th>`)
|
|
1471
|
+
.join('');
|
|
1472
|
+
const bodyRowsHtml = tableRows
|
|
1473
|
+
.map((row) => {
|
|
1474
|
+
const cells = keys
|
|
1475
|
+
.map((k) => {
|
|
1476
|
+
const val = row[k];
|
|
1477
|
+
const display = val === undefined ? '' : escapeHtml(String(val));
|
|
1478
|
+
return `<td style="padding: 2px 8px; border: 1px solid #ddd; font-size: 11px; white-space: nowrap;">${display}</td>`;
|
|
1479
|
+
})
|
|
1480
|
+
.join('');
|
|
1481
|
+
return `<tr>${cells}</tr>`;
|
|
1482
|
+
})
|
|
1483
|
+
.join('');
|
|
1484
|
+
argsHTML = `<table style="border-collapse: collapse; margin: 2px 0; font-family: monospace;"><thead><tr>${headerCells}</tr></thead><tbody>${bodyRowsHtml}</tbody></table>`;
|
|
1485
|
+
}
|
|
1486
|
+
else if (entry.args.length > 0) {
|
|
1408
1487
|
argsHTML = entry.args
|
|
1409
1488
|
.map((arg, argIdx) => {
|
|
1410
1489
|
if (typeof arg === 'object' && arg !== null) {
|
|
@@ -1460,15 +1539,29 @@ export function createGgPlugin(options, gg) {
|
|
|
1460
1539
|
}
|
|
1461
1540
|
}
|
|
1462
1541
|
const fileTitle = fileTitleText ? ` title="${escapeHtml(fileTitleText)}"` : '';
|
|
1542
|
+
// Level class for warn/error styling
|
|
1543
|
+
const levelClass = entry.level === 'warn'
|
|
1544
|
+
? ' gg-level-warn'
|
|
1545
|
+
: entry.level === 'error'
|
|
1546
|
+
? ' gg-level-error'
|
|
1547
|
+
: '';
|
|
1548
|
+
// Stack trace toggle (for error/trace entries with captured stacks)
|
|
1549
|
+
let stackHTML = '';
|
|
1550
|
+
if (entry.stack) {
|
|
1551
|
+
const stackId = `stack-${index}`;
|
|
1552
|
+
stackHTML =
|
|
1553
|
+
`<span class="gg-stack-toggle" data-stack-id="${stackId}">▶ stack</span>` +
|
|
1554
|
+
`<div class="gg-stack-content" data-stack-id="${stackId}">${escapeHtml(entry.stack)}</div>`;
|
|
1555
|
+
}
|
|
1463
1556
|
// Desktop: grid layout, Mobile: stacked layout
|
|
1464
|
-
return (`<div class="gg-log-entry">` +
|
|
1557
|
+
return (`<div class="gg-log-entry${levelClass}">` +
|
|
1465
1558
|
`<div class="gg-log-header">` +
|
|
1466
1559
|
iconsCol +
|
|
1467
1560
|
`<div class="gg-log-diff${soloClass}" style="color: ${color};"${soloAttr}>${diff}</div>` +
|
|
1468
1561
|
`<div class="gg-log-ns${soloClass}" style="color: ${color};"${soloAttr}${fileAttr}${lineAttr}${colAttr}${fileTitle}>${ns}</div>` +
|
|
1469
1562
|
`<div class="gg-log-handle"></div>` +
|
|
1470
1563
|
`</div>` +
|
|
1471
|
-
`<div class="gg-log-content"${entry.src?.trim() && !/^['"`]/.test(entry.src) ? ` data-src="${escapeHtml(entry.src)}"` : ''}>${argsHTML}</div>` +
|
|
1564
|
+
`<div class="gg-log-content"${!entry.level && entry.src?.trim() && !/^['"`]/.test(entry.src) ? ` data-src="${escapeHtml(entry.src)}"` : ''}>${argsHTML}${stackHTML}</div>` +
|
|
1472
1565
|
detailsHTML +
|
|
1473
1566
|
`</div>`);
|
|
1474
1567
|
})
|
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' | '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,16 @@ 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.warn(') &&
|
|
45
|
+
!code.includes('gg.error(') &&
|
|
46
|
+
!code.includes('gg.table(') &&
|
|
47
|
+
!code.includes('gg.trace(') &&
|
|
48
|
+
!code.includes('gg.assert(') &&
|
|
49
|
+
!code.includes('gg.time(') &&
|
|
50
|
+
!code.includes('gg.timeLog(') &&
|
|
51
|
+
!code.includes('gg.timeEnd('))
|
|
43
52
|
return null;
|
|
44
53
|
// Don't transform gg's own source files
|
|
45
54
|
if (id.includes('/lib/gg.') || id.includes('/lib/debug'))
|
|
@@ -122,7 +131,7 @@ export function collectCodeRanges(code) {
|
|
|
122
131
|
const functionScopes = [];
|
|
123
132
|
// Script blocks (instance + module)
|
|
124
133
|
// The Svelte AST Program node has start/end at runtime but TypeScript's
|
|
125
|
-
// estree Program type doesn't declare them —
|
|
134
|
+
// estree Program type doesn't declare them — we know they exist.
|
|
126
135
|
if (ast.instance) {
|
|
127
136
|
const content = ast.instance.content;
|
|
128
137
|
ranges.push({ start: content.start, end: content.end, context: 'script' });
|
|
@@ -643,61 +652,68 @@ export function transformGgCalls(code, shortPath, filePath, svelteInfo, jsFuncti
|
|
|
643
652
|
// States for string/comment tracking
|
|
644
653
|
let i = 0;
|
|
645
654
|
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
|
-
|
|
655
|
+
// For .svelte files, only apply JS string/comment/backtick skipping inside
|
|
656
|
+
// code ranges (script blocks + template expressions). Outside code ranges,
|
|
657
|
+
// characters like ' " ` // /* are just HTML prose — NOT JS syntax.
|
|
658
|
+
// e.g. "Eruda's" contains an apostrophe that is NOT a JS string delimiter.
|
|
659
|
+
const inCodeRange = !svelteInfo || !!rangeAt(i);
|
|
660
|
+
if (inCodeRange) {
|
|
661
|
+
// Skip single-line comments
|
|
662
|
+
if (code[i] === '/' && code[i + 1] === '/') {
|
|
663
|
+
const end = code.indexOf('\n', i);
|
|
664
|
+
i = end === -1 ? code.length : end + 1;
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
// Skip multi-line comments
|
|
668
|
+
if (code[i] === '/' && code[i + 1] === '*') {
|
|
669
|
+
const end = code.indexOf('*/', i + 2);
|
|
670
|
+
i = end === -1 ? code.length : end + 2;
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
// Skip template literals (backticks)
|
|
674
|
+
if (code[i] === '`') {
|
|
675
|
+
i++;
|
|
676
|
+
let depth = 0;
|
|
677
|
+
while (i < code.length) {
|
|
678
|
+
if (code[i] === '\\') {
|
|
679
|
+
i += 2;
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
if (code[i] === '$' && code[i + 1] === '{') {
|
|
683
|
+
depth++;
|
|
684
|
+
i += 2;
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
if (code[i] === '}' && depth > 0) {
|
|
688
|
+
depth--;
|
|
689
|
+
i++;
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
if (code[i] === '`' && depth === 0) {
|
|
693
|
+
i++;
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
678
696
|
i++;
|
|
679
|
-
break;
|
|
680
697
|
}
|
|
681
|
-
|
|
698
|
+
continue;
|
|
682
699
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
700
|
+
// Skip strings (single and double quotes)
|
|
701
|
+
if (code[i] === '"' || code[i] === "'") {
|
|
702
|
+
const quote = code[i];
|
|
703
|
+
i++;
|
|
704
|
+
while (i < code.length) {
|
|
705
|
+
if (code[i] === '\\') {
|
|
706
|
+
i += 2;
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
if (code[i] === quote) {
|
|
710
|
+
i++;
|
|
711
|
+
break;
|
|
712
|
+
}
|
|
695
713
|
i++;
|
|
696
|
-
break;
|
|
697
714
|
}
|
|
698
|
-
|
|
715
|
+
continue;
|
|
699
716
|
}
|
|
700
|
-
continue;
|
|
701
717
|
}
|
|
702
718
|
// Look for 'gg' pattern — could be gg( or gg.ns(
|
|
703
719
|
if (code[i] === 'g' && code[i + 1] === 'g') {
|
|
@@ -783,7 +799,44 @@ export function transformGgCalls(code, shortPath, filePath, svelteInfo, jsFuncti
|
|
|
783
799
|
i += 6;
|
|
784
800
|
continue;
|
|
785
801
|
}
|
|
786
|
-
//
|
|
802
|
+
// Case 1b: gg.warn/error/table/trace/assert → gg._warn/_error/_table/_trace/_assert
|
|
803
|
+
// These methods are rewritten like bare gg() but with their internal variant.
|
|
804
|
+
const dotMethodMatch = code
|
|
805
|
+
.slice(i + 2)
|
|
806
|
+
.match(/^\.(warn|error|table|trace|assert|time|timeLog|timeEnd)\(/);
|
|
807
|
+
if (dotMethodMatch) {
|
|
808
|
+
const methodName = dotMethodMatch[1];
|
|
809
|
+
const internalName = `_${methodName}`;
|
|
810
|
+
const methodCallLen = 2 + 1 + methodName.length + 1; // 'gg' + '.' + method + '('
|
|
811
|
+
const openParenPos = i + methodCallLen - 1;
|
|
812
|
+
const { line, col } = getLineCol(code, i);
|
|
813
|
+
const fnName = getFunctionName(i, range);
|
|
814
|
+
const callpoint = `${shortPath}${fnName ? `@${fnName}` : ''}`;
|
|
815
|
+
const escapedNs = escapeForString(callpoint);
|
|
816
|
+
const closeParenPos = findMatchingParen(code, openParenPos);
|
|
817
|
+
if (closeParenPos === -1) {
|
|
818
|
+
i += methodCallLen;
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
const argsText = code.slice(openParenPos + 1, closeParenPos).trim();
|
|
822
|
+
result.push(code.slice(lastIndex, i));
|
|
823
|
+
if (argsText === '') {
|
|
824
|
+
// gg.warn() → gg._warn(opts)
|
|
825
|
+
result.push(`gg.${internalName}(${buildOptions(range, escapedNs, line, col)})`);
|
|
826
|
+
lastIndex = closeParenPos + 1;
|
|
827
|
+
i = closeParenPos + 1;
|
|
828
|
+
}
|
|
829
|
+
else {
|
|
830
|
+
// gg.warn(expr) → gg._warn(opts, expr)
|
|
831
|
+
const escapedSrc = escapeForString(argsText);
|
|
832
|
+
result.push(`gg.${internalName}(${buildOptions(range, escapedNs, line, col, escapedSrc)}, `);
|
|
833
|
+
lastIndex = openParenPos + 1; // keep original args
|
|
834
|
+
i = openParenPos + 1;
|
|
835
|
+
}
|
|
836
|
+
modified = true;
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
// Skip other gg.* calls (gg.enable, gg.disable, gg._ns, gg._onLog, gg.time, etc.)
|
|
787
840
|
if (code[i + 2] === '.') {
|
|
788
841
|
i += 3;
|
|
789
842
|
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' | '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,70 @@ export declare namespace gg {
|
|
|
101
110
|
col?: number;
|
|
102
111
|
src?: string;
|
|
103
112
|
};
|
|
113
|
+
let warn: (...args: unknown[]) => unknown;
|
|
114
|
+
let error: (...args: unknown[]) => unknown;
|
|
115
|
+
let assert: (condition: unknown, ...args: unknown[]) => unknown;
|
|
116
|
+
let table: (data: unknown, columns?: string[]) => unknown;
|
|
117
|
+
let time: (label?: string) => void;
|
|
118
|
+
let timeLog: (label?: string, ...args: unknown[]) => void;
|
|
119
|
+
let timeEnd: (label?: string) => void;
|
|
120
|
+
let trace: (...args: unknown[]) => unknown;
|
|
121
|
+
let _warn: (options: {
|
|
122
|
+
ns: string;
|
|
123
|
+
file?: string;
|
|
124
|
+
line?: number;
|
|
125
|
+
col?: number;
|
|
126
|
+
src?: string;
|
|
127
|
+
}, ...args: unknown[]) => unknown;
|
|
128
|
+
let _error: (options: {
|
|
129
|
+
ns: string;
|
|
130
|
+
file?: string;
|
|
131
|
+
line?: number;
|
|
132
|
+
col?: number;
|
|
133
|
+
src?: string;
|
|
134
|
+
}, ...args: unknown[]) => unknown;
|
|
135
|
+
let _assert: (options: {
|
|
136
|
+
ns: string;
|
|
137
|
+
file?: string;
|
|
138
|
+
line?: number;
|
|
139
|
+
col?: number;
|
|
140
|
+
src?: string;
|
|
141
|
+
}, condition: unknown, ...args: unknown[]) => unknown;
|
|
142
|
+
let _table: (options: {
|
|
143
|
+
ns: string;
|
|
144
|
+
file?: string;
|
|
145
|
+
line?: number;
|
|
146
|
+
col?: number;
|
|
147
|
+
src?: string;
|
|
148
|
+
}, data: unknown, columns?: string[]) => unknown;
|
|
149
|
+
let _trace: (options: {
|
|
150
|
+
ns: string;
|
|
151
|
+
file?: string;
|
|
152
|
+
line?: number;
|
|
153
|
+
col?: number;
|
|
154
|
+
src?: string;
|
|
155
|
+
}, ...args: unknown[]) => unknown;
|
|
156
|
+
let _time: (options: {
|
|
157
|
+
ns: string;
|
|
158
|
+
file?: string;
|
|
159
|
+
line?: number;
|
|
160
|
+
col?: number;
|
|
161
|
+
src?: string;
|
|
162
|
+
}, label?: string) => void;
|
|
163
|
+
let _timeLog: (options: {
|
|
164
|
+
ns: string;
|
|
165
|
+
file?: string;
|
|
166
|
+
line?: number;
|
|
167
|
+
col?: number;
|
|
168
|
+
src?: string;
|
|
169
|
+
}, label?: string, ...args: unknown[]) => void;
|
|
170
|
+
let _timeEnd: (options: {
|
|
171
|
+
ns: string;
|
|
172
|
+
file?: string;
|
|
173
|
+
line?: number;
|
|
174
|
+
col?: number;
|
|
175
|
+
src?: string;
|
|
176
|
+
}, label?: string) => void;
|
|
104
177
|
}
|
|
105
178
|
/**
|
|
106
179
|
* 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,13 @@ 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 warn/error
|
|
286
|
+
if (level === 'warn') {
|
|
287
|
+
logArgs[0] = `⚠️ ${logArgs[0]}`;
|
|
288
|
+
}
|
|
289
|
+
else if (level === 'error') {
|
|
290
|
+
logArgs[0] = `⛔ ${logArgs[0]}`;
|
|
291
|
+
}
|
|
336
292
|
// Log to console via debug
|
|
337
293
|
if (logArgs.length === 1) {
|
|
338
294
|
ggLogFunction(logArgs[0]);
|
|
@@ -351,7 +307,10 @@ gg._ns = function (options, ...args) {
|
|
|
351
307
|
file,
|
|
352
308
|
line,
|
|
353
309
|
col,
|
|
354
|
-
src
|
|
310
|
+
src,
|
|
311
|
+
level,
|
|
312
|
+
stack,
|
|
313
|
+
tableData
|
|
355
314
|
};
|
|
356
315
|
if (_onLogCallback) {
|
|
357
316
|
_onLogCallback(entry);
|
|
@@ -360,6 +319,21 @@ gg._ns = function (options, ...args) {
|
|
|
360
319
|
earlyLogBuffer.push(entry);
|
|
361
320
|
}
|
|
362
321
|
return returnValue;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* gg._ns() - Internal: log with namespace and source file metadata.
|
|
325
|
+
*
|
|
326
|
+
* Called by the ggCallSitesPlugin Vite plugin, which rewrites both bare gg()
|
|
327
|
+
* calls and manual gg.ns() calls to gg._ns({ns, file, line, col}, ...) at
|
|
328
|
+
* build time. This gives each call site a unique namespace plus the source
|
|
329
|
+
* location for open-in-editor support.
|
|
330
|
+
*
|
|
331
|
+
* @param options - { ns: string; file?: string; line?: number; col?: number }
|
|
332
|
+
* @param args - Same arguments as gg()
|
|
333
|
+
* @returns Same as gg() - the first arg, or call-site info if no args
|
|
334
|
+
*/
|
|
335
|
+
gg._ns = function (options, ...args) {
|
|
336
|
+
return ggLog(options, ...args);
|
|
363
337
|
};
|
|
364
338
|
/**
|
|
365
339
|
* gg._o() - Internal: build options object for gg._ns() without object literal syntax.
|
|
@@ -390,6 +364,357 @@ gg.clearPersist = () => {
|
|
|
390
364
|
}
|
|
391
365
|
}
|
|
392
366
|
};
|
|
367
|
+
// ── Console-like methods ───────────────────────────────────────────────
|
|
368
|
+
// Each public method (gg.warn, gg.error, etc.) has a corresponding internal
|
|
369
|
+
// method (gg._warn, gg._error, etc.) that accepts call-site metadata from
|
|
370
|
+
// the Vite plugin. The public methods use runtime stack-based callpoints
|
|
371
|
+
// as a fallback when the plugin isn't installed.
|
|
372
|
+
/**
|
|
373
|
+
* Capture a cleaned-up stack trace, stripping internal gg frames.
|
|
374
|
+
* @param skipFrames - Number of internal frames to strip from the top
|
|
375
|
+
*/
|
|
376
|
+
function captureStack(skipFrames) {
|
|
377
|
+
let stack = new Error().stack || undefined;
|
|
378
|
+
if (stack) {
|
|
379
|
+
const lines = stack.split('\n');
|
|
380
|
+
stack = lines.slice(skipFrames).join('\n');
|
|
381
|
+
}
|
|
382
|
+
return stack;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Get stack from an Error arg or capture a fresh one.
|
|
386
|
+
*/
|
|
387
|
+
function getErrorStack(firstArg, skipFrames) {
|
|
388
|
+
if (firstArg instanceof Error && firstArg.stack) {
|
|
389
|
+
return firstArg.stack;
|
|
390
|
+
}
|
|
391
|
+
return captureStack(skipFrames);
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* gg.warn() - Log at warning level.
|
|
395
|
+
*
|
|
396
|
+
* Passthrough: returns the first argument.
|
|
397
|
+
* In Eruda, entries are styled with a yellow/warning indicator.
|
|
398
|
+
*
|
|
399
|
+
* @example
|
|
400
|
+
* gg.warn('deprecated API used');
|
|
401
|
+
* const result = gg.warn(computeValue(), 'might be slow');
|
|
402
|
+
*/
|
|
403
|
+
gg.warn = function (...args) {
|
|
404
|
+
if (!ggConfig.enabled || isCloudflareWorker()) {
|
|
405
|
+
return args.length ? args[0] : undefined;
|
|
406
|
+
}
|
|
407
|
+
const callpoint = resolveCallpoint(3);
|
|
408
|
+
return ggLog({ ns: callpoint, level: 'warn' }, ...args);
|
|
409
|
+
};
|
|
410
|
+
/**
|
|
411
|
+
* gg._warn() - Internal: warn with call-site metadata from Vite plugin.
|
|
412
|
+
*/
|
|
413
|
+
gg._warn = function (options, ...args) {
|
|
414
|
+
return ggLog({ ...options, level: 'warn' }, ...args);
|
|
415
|
+
};
|
|
416
|
+
/**
|
|
417
|
+
* gg.error() - Log at error level.
|
|
418
|
+
*
|
|
419
|
+
* Passthrough: returns the first argument.
|
|
420
|
+
* Captures a stack trace silently — visible in Eruda via a collapsible toggle.
|
|
421
|
+
* If the first argument is an Error object, its .stack is used instead.
|
|
422
|
+
*
|
|
423
|
+
* @example
|
|
424
|
+
* gg.error('connection failed');
|
|
425
|
+
* gg.error(new Error('timeout'));
|
|
426
|
+
* const val = gg.error(response, 'unexpected status');
|
|
427
|
+
*/
|
|
428
|
+
gg.error = function (...args) {
|
|
429
|
+
if (!ggConfig.enabled || isCloudflareWorker()) {
|
|
430
|
+
return args.length ? args[0] : undefined;
|
|
431
|
+
}
|
|
432
|
+
const callpoint = resolveCallpoint(3);
|
|
433
|
+
const stack = getErrorStack(args[0], 4);
|
|
434
|
+
return ggLog({ ns: callpoint, level: 'error', stack }, ...args);
|
|
435
|
+
};
|
|
436
|
+
/**
|
|
437
|
+
* gg._error() - Internal: error with call-site metadata from Vite plugin.
|
|
438
|
+
*/
|
|
439
|
+
gg._error = function (options, ...args) {
|
|
440
|
+
const stack = getErrorStack(args[0], 3);
|
|
441
|
+
return ggLog({ ...options, level: 'error', stack }, ...args);
|
|
442
|
+
};
|
|
443
|
+
/**
|
|
444
|
+
* gg.assert() - Log only if condition is false.
|
|
445
|
+
*
|
|
446
|
+
* Like console.assert: if the first argument is falsy, logs the remaining
|
|
447
|
+
* arguments at error level. If the condition is truthy, does nothing.
|
|
448
|
+
* Passthrough: always returns the condition value.
|
|
449
|
+
*
|
|
450
|
+
* @example
|
|
451
|
+
* gg.assert(user != null, 'user should exist');
|
|
452
|
+
* gg.assert(list.length > 0, 'list is empty', list);
|
|
453
|
+
*/
|
|
454
|
+
gg.assert = function (condition, ...args) {
|
|
455
|
+
if (!condition) {
|
|
456
|
+
if (!ggConfig.enabled || isCloudflareWorker())
|
|
457
|
+
return condition;
|
|
458
|
+
const callpoint = resolveCallpoint(3);
|
|
459
|
+
const stack = captureStack(4);
|
|
460
|
+
const assertArgs = args.length > 0 ? args : ['Assertion failed'];
|
|
461
|
+
ggLog({ ns: callpoint, level: 'error', stack }, ...assertArgs);
|
|
462
|
+
}
|
|
463
|
+
return condition;
|
|
464
|
+
};
|
|
465
|
+
/**
|
|
466
|
+
* gg._assert() - Internal: assert with call-site metadata from Vite plugin.
|
|
467
|
+
*/
|
|
468
|
+
gg._assert = function (options, condition, ...args) {
|
|
469
|
+
if (!condition) {
|
|
470
|
+
if (!ggConfig.enabled || isCloudflareWorker())
|
|
471
|
+
return condition;
|
|
472
|
+
const stack = captureStack(3);
|
|
473
|
+
const assertArgs = args.length > 0 ? args : ['Assertion failed'];
|
|
474
|
+
ggLog({ ...options, level: 'error', stack }, ...assertArgs);
|
|
475
|
+
}
|
|
476
|
+
return condition;
|
|
477
|
+
};
|
|
478
|
+
/**
|
|
479
|
+
* gg.table() - Log tabular data.
|
|
480
|
+
*
|
|
481
|
+
* Formats an array of objects (or an object of objects) as an ASCII table.
|
|
482
|
+
* Passthrough: returns the data argument.
|
|
483
|
+
*
|
|
484
|
+
* @example
|
|
485
|
+
* gg.table([{name: 'Alice', age: 30}, {name: 'Bob', age: 25}]);
|
|
486
|
+
* gg.table({a: {x: 1}, b: {x: 2}});
|
|
487
|
+
*/
|
|
488
|
+
gg.table = function (data, columns) {
|
|
489
|
+
if (!ggConfig.enabled || isCloudflareWorker())
|
|
490
|
+
return data;
|
|
491
|
+
const callpoint = resolveCallpoint(3);
|
|
492
|
+
const { keys, rows } = formatTable(data, columns);
|
|
493
|
+
ggLog({ ns: callpoint, tableData: { keys, rows } }, '(table)');
|
|
494
|
+
// Also emit a native console.table for proper rendering in browser/Node consoles
|
|
495
|
+
if (columns) {
|
|
496
|
+
console.table(data, columns);
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
console.table(data);
|
|
500
|
+
}
|
|
501
|
+
return data;
|
|
502
|
+
};
|
|
503
|
+
/**
|
|
504
|
+
* gg._table() - Internal: table with call-site metadata from Vite plugin.
|
|
505
|
+
*/
|
|
506
|
+
gg._table = function (options, data, columns) {
|
|
507
|
+
if (!ggConfig.enabled || isCloudflareWorker())
|
|
508
|
+
return data;
|
|
509
|
+
const { keys, rows } = formatTable(data, columns);
|
|
510
|
+
ggLog({ ...options, tableData: { keys, rows } }, '(table)');
|
|
511
|
+
if (columns) {
|
|
512
|
+
console.table(data, columns);
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
console.table(data);
|
|
516
|
+
}
|
|
517
|
+
return data;
|
|
518
|
+
};
|
|
519
|
+
// Timer storage for gg.time / gg.timeEnd / gg.timeLog
|
|
520
|
+
const timers = new Map();
|
|
521
|
+
/**
|
|
522
|
+
* gg.time() - Start a named timer.
|
|
523
|
+
*
|
|
524
|
+
* @example
|
|
525
|
+
* gg.time('fetch');
|
|
526
|
+
* const data = await fetchData();
|
|
527
|
+
* gg.timeEnd('fetch'); // logs "+123ms fetch: 456ms"
|
|
528
|
+
*/
|
|
529
|
+
gg.time = function (label = 'default') {
|
|
530
|
+
if (!ggConfig.enabled || isCloudflareWorker())
|
|
531
|
+
return;
|
|
532
|
+
timers.set(label, performance.now());
|
|
533
|
+
};
|
|
534
|
+
/** gg._time() - Internal: time with call-site metadata from Vite plugin. */
|
|
535
|
+
gg._time = function (_options, label = 'default') {
|
|
536
|
+
if (!ggConfig.enabled || isCloudflareWorker())
|
|
537
|
+
return;
|
|
538
|
+
timers.set(label, performance.now());
|
|
539
|
+
};
|
|
540
|
+
/**
|
|
541
|
+
* gg.timeLog() - Log the current elapsed time without stopping the timer.
|
|
542
|
+
*
|
|
543
|
+
* @example
|
|
544
|
+
* gg.time('process');
|
|
545
|
+
* // ... step 1 ...
|
|
546
|
+
* gg.timeLog('process', 'step 1 done');
|
|
547
|
+
* // ... step 2 ...
|
|
548
|
+
* gg.timeEnd('process');
|
|
549
|
+
*/
|
|
550
|
+
gg.timeLog = function (label = 'default', ...args) {
|
|
551
|
+
if (!ggConfig.enabled || isCloudflareWorker())
|
|
552
|
+
return;
|
|
553
|
+
const start = timers.get(label);
|
|
554
|
+
if (start === undefined) {
|
|
555
|
+
const callpoint = resolveCallpoint(3);
|
|
556
|
+
ggLog({ ns: callpoint, level: 'warn' }, `Timer '${label}' does not exist`);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
const elapsed = performance.now() - start;
|
|
560
|
+
const callpoint = resolveCallpoint(3);
|
|
561
|
+
ggLog({ ns: callpoint }, `${label}: ${formatElapsed(elapsed)}`, ...args);
|
|
562
|
+
};
|
|
563
|
+
/** gg._timeLog() - Internal: timeLog with call-site metadata from Vite plugin. */
|
|
564
|
+
gg._timeLog = function (options, label = 'default', ...args) {
|
|
565
|
+
if (!ggConfig.enabled || isCloudflareWorker())
|
|
566
|
+
return;
|
|
567
|
+
const start = timers.get(label);
|
|
568
|
+
if (start === undefined) {
|
|
569
|
+
ggLog({ ...options, level: 'warn' }, `Timer '${label}' does not exist`);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
const elapsed = performance.now() - start;
|
|
573
|
+
ggLog(options, `${label}: ${formatElapsed(elapsed)}`, ...args);
|
|
574
|
+
};
|
|
575
|
+
/**
|
|
576
|
+
* gg.timeEnd() - Stop a named timer and log the elapsed time.
|
|
577
|
+
*
|
|
578
|
+
* @example
|
|
579
|
+
* gg.time('fetch');
|
|
580
|
+
* const data = await fetchData();
|
|
581
|
+
* gg.timeEnd('fetch'); // logs "fetch: 456.12ms"
|
|
582
|
+
*/
|
|
583
|
+
gg.timeEnd = function (label = 'default') {
|
|
584
|
+
if (!ggConfig.enabled || isCloudflareWorker())
|
|
585
|
+
return;
|
|
586
|
+
const start = timers.get(label);
|
|
587
|
+
if (start === undefined) {
|
|
588
|
+
const callpoint = resolveCallpoint(3);
|
|
589
|
+
ggLog({ ns: callpoint, level: 'warn' }, `Timer '${label}' does not exist`);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const elapsed = performance.now() - start;
|
|
593
|
+
timers.delete(label);
|
|
594
|
+
const callpoint = resolveCallpoint(3);
|
|
595
|
+
ggLog({ ns: callpoint }, `${label}: ${formatElapsed(elapsed)}`);
|
|
596
|
+
};
|
|
597
|
+
/** gg._timeEnd() - Internal: timeEnd with call-site metadata from Vite plugin. */
|
|
598
|
+
gg._timeEnd = function (options, label = 'default') {
|
|
599
|
+
if (!ggConfig.enabled || isCloudflareWorker())
|
|
600
|
+
return;
|
|
601
|
+
const start = timers.get(label);
|
|
602
|
+
if (start === undefined) {
|
|
603
|
+
ggLog({ ...options, level: 'warn' }, `Timer '${label}' does not exist`);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
const elapsed = performance.now() - start;
|
|
607
|
+
timers.delete(label);
|
|
608
|
+
ggLog(options, `${label}: ${formatElapsed(elapsed)}`);
|
|
609
|
+
};
|
|
610
|
+
/**
|
|
611
|
+
* gg.trace() - Log with a stack trace.
|
|
612
|
+
*
|
|
613
|
+
* Like console.trace: logs the arguments plus a full stack trace.
|
|
614
|
+
* Passthrough: returns the first argument.
|
|
615
|
+
*
|
|
616
|
+
* @example
|
|
617
|
+
* gg.trace('how did we get here?');
|
|
618
|
+
* const val = gg.trace(result, 'call path');
|
|
619
|
+
*/
|
|
620
|
+
gg.trace = function (...args) {
|
|
621
|
+
if (!ggConfig.enabled || isCloudflareWorker()) {
|
|
622
|
+
return args.length ? args[0] : undefined;
|
|
623
|
+
}
|
|
624
|
+
const callpoint = resolveCallpoint(3);
|
|
625
|
+
const stack = captureStack(4);
|
|
626
|
+
const traceArgs = args.length > 0 ? args : ['Trace'];
|
|
627
|
+
return ggLog({ ns: callpoint, stack }, ...traceArgs);
|
|
628
|
+
};
|
|
629
|
+
/**
|
|
630
|
+
* gg._trace() - Internal: trace with call-site metadata from Vite plugin.
|
|
631
|
+
*/
|
|
632
|
+
gg._trace = function (options, ...args) {
|
|
633
|
+
if (!ggConfig.enabled || isCloudflareWorker()) {
|
|
634
|
+
return args.length ? args[0] : undefined;
|
|
635
|
+
}
|
|
636
|
+
const stack = captureStack(3);
|
|
637
|
+
const traceArgs = args.length > 0 ? args : ['Trace'];
|
|
638
|
+
return ggLog({ ...options, stack }, ...traceArgs);
|
|
639
|
+
};
|
|
640
|
+
/**
|
|
641
|
+
* Format elapsed time with appropriate precision.
|
|
642
|
+
* < 1s → "123.45ms", >= 1s → "1.23s", >= 60s → "1m 2.3s"
|
|
643
|
+
*/
|
|
644
|
+
function formatElapsed(ms) {
|
|
645
|
+
if (ms < 1000)
|
|
646
|
+
return `${ms.toFixed(2)}ms`;
|
|
647
|
+
if (ms < 60000)
|
|
648
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
649
|
+
const minutes = Math.floor(ms / 60000);
|
|
650
|
+
const seconds = (ms % 60000) / 1000;
|
|
651
|
+
return `${minutes}m ${seconds.toFixed(1)}s`;
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Normalize data into structured keys + rows for table rendering.
|
|
655
|
+
* Used by both Eruda (HTML table) and console.table() delegation.
|
|
656
|
+
* Supports arrays of objects, arrays of primitives, and objects of objects.
|
|
657
|
+
*/
|
|
658
|
+
function formatTable(data, columns) {
|
|
659
|
+
if (data === null || data === undefined || typeof data !== 'object') {
|
|
660
|
+
return { keys: [], rows: [] };
|
|
661
|
+
}
|
|
662
|
+
// Normalize to rows: [{key, ...values}]
|
|
663
|
+
let rows;
|
|
664
|
+
let allKeys;
|
|
665
|
+
if (Array.isArray(data)) {
|
|
666
|
+
if (data.length === 0)
|
|
667
|
+
return { keys: [], rows: [] };
|
|
668
|
+
// Array of primitives
|
|
669
|
+
if (typeof data[0] !== 'object' || data[0] === null) {
|
|
670
|
+
allKeys = ['(index)', 'Value'];
|
|
671
|
+
rows = data.map((v, i) => ({ '(index)': i, Value: v }));
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
// Array of objects
|
|
675
|
+
const keySet = new Set();
|
|
676
|
+
keySet.add('(index)');
|
|
677
|
+
for (const item of data) {
|
|
678
|
+
if (item && typeof item === 'object') {
|
|
679
|
+
Object.keys(item).forEach((k) => keySet.add(k));
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
allKeys = Array.from(keySet);
|
|
683
|
+
rows = data.map((item, i) => ({
|
|
684
|
+
'(index)': i,
|
|
685
|
+
...(item && typeof item === 'object' ? item : { Value: item })
|
|
686
|
+
}));
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
// Object of objects/values
|
|
691
|
+
const entries = Object.entries(data);
|
|
692
|
+
if (entries.length === 0)
|
|
693
|
+
return { keys: [], rows: [] };
|
|
694
|
+
const keySet = new Set();
|
|
695
|
+
keySet.add('(index)');
|
|
696
|
+
for (const [, val] of entries) {
|
|
697
|
+
if (val && typeof val === 'object' && !Array.isArray(val)) {
|
|
698
|
+
Object.keys(val).forEach((k) => keySet.add(k));
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
keySet.add('Value');
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
allKeys = Array.from(keySet);
|
|
705
|
+
rows = entries.map(([key, val]) => ({
|
|
706
|
+
'(index)': key,
|
|
707
|
+
...(val && typeof val === 'object' && !Array.isArray(val)
|
|
708
|
+
? val
|
|
709
|
+
: { Value: val })
|
|
710
|
+
}));
|
|
711
|
+
}
|
|
712
|
+
// Apply column filter
|
|
713
|
+
if (columns && columns.length > 0) {
|
|
714
|
+
allKeys = ['(index)', ...columns.filter((c) => allKeys.includes(c))];
|
|
715
|
+
}
|
|
716
|
+
return { keys: allKeys, rows };
|
|
717
|
+
}
|
|
393
718
|
/**
|
|
394
719
|
* Parse color string to RGB values
|
|
395
720
|
* Accepts: named colors, hex (#rgb, #rrggbb), rgb(r,g,b), rgba(r,g,b,a)
|