@leftium/gg 0.0.45 → 0.0.46
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/dist/GgConsole.svelte +2 -2
- package/dist/eruda/buffer.d.ts +22 -5
- package/dist/eruda/buffer.js +57 -14
- package/dist/eruda/loader.js +5 -3
- package/dist/eruda/plugin.js +270 -89
- package/dist/eruda/types.d.ts +5 -0
- package/package.json +2 -1
package/dist/GgConsole.svelte
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
import { onMount } from 'svelte';
|
|
3
3
|
import type { GgErudaOptions } from './eruda/types.js';
|
|
4
4
|
|
|
5
|
-
let { prod, maxEntries, erudaOptions }: GgErudaOptions = $props();
|
|
5
|
+
let { prod, maxEntries, erudaOptions, open }: GgErudaOptions = $props();
|
|
6
6
|
|
|
7
7
|
onMount(() => {
|
|
8
8
|
import('./eruda/index.js').then(({ initGgEruda }) => {
|
|
9
|
-
initGgEruda({ prod, maxEntries, erudaOptions });
|
|
9
|
+
initGgEruda({ prod, maxEntries, erudaOptions, open });
|
|
10
10
|
});
|
|
11
11
|
});
|
|
12
12
|
</script>
|
package/dist/eruda/buffer.d.ts
CHANGED
|
@@ -1,19 +1,36 @@
|
|
|
1
1
|
import type { CapturedEntry } from './types.js';
|
|
2
2
|
/**
|
|
3
|
-
* Ring buffer for captured log entries
|
|
3
|
+
* Ring buffer for captured log entries.
|
|
4
|
+
*
|
|
5
|
+
* Uses a fixed-size array with a head pointer so that push() is always O(1).
|
|
6
|
+
* The previous implementation used Array.push + Array.shift which is O(n)
|
|
7
|
+
* once the buffer is full (every shift copies all elements forward).
|
|
4
8
|
*/
|
|
5
9
|
export declare class LogBuffer {
|
|
6
|
-
private
|
|
10
|
+
private buf;
|
|
11
|
+
private head;
|
|
12
|
+
private count;
|
|
7
13
|
private maxSize;
|
|
8
14
|
private _totalPushed;
|
|
9
15
|
constructor(maxSize?: number);
|
|
10
16
|
/**
|
|
11
|
-
* Add an entry to the buffer
|
|
12
|
-
* If buffer is full, oldest entry is
|
|
17
|
+
* Add an entry to the buffer — O(1) always.
|
|
18
|
+
* If buffer is full, the oldest entry is overwritten.
|
|
13
19
|
*/
|
|
14
20
|
push(entry: CapturedEntry): void;
|
|
15
21
|
/**
|
|
16
|
-
* Get
|
|
22
|
+
* Get a single entry by logical index (0 = oldest). O(1).
|
|
23
|
+
* Returns undefined if index is out of range.
|
|
24
|
+
*/
|
|
25
|
+
get(index: number): CapturedEntry | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Get a range of entries [start, end) by logical index. O(end - start).
|
|
28
|
+
* Clamps to valid range. Returns a new array.
|
|
29
|
+
*/
|
|
30
|
+
getRange(start: number, end: number): CapturedEntry[];
|
|
31
|
+
/**
|
|
32
|
+
* Get all entries in insertion order (optionally filtered).
|
|
33
|
+
* Allocates a new array — used for full renders, not the hot path.
|
|
17
34
|
*/
|
|
18
35
|
getEntries(filter?: (entry: CapturedEntry) => boolean): CapturedEntry[];
|
|
19
36
|
/**
|
package/dist/eruda/buffer.js
CHANGED
|
@@ -1,44 +1,87 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Ring buffer for captured log entries
|
|
2
|
+
* Ring buffer for captured log entries.
|
|
3
|
+
*
|
|
4
|
+
* Uses a fixed-size array with a head pointer so that push() is always O(1).
|
|
5
|
+
* The previous implementation used Array.push + Array.shift which is O(n)
|
|
6
|
+
* once the buffer is full (every shift copies all elements forward).
|
|
3
7
|
*/
|
|
4
8
|
export class LogBuffer {
|
|
5
|
-
|
|
9
|
+
buf;
|
|
10
|
+
head = 0; // index of the oldest entry
|
|
11
|
+
count = 0; // number of live entries
|
|
6
12
|
maxSize;
|
|
7
13
|
_totalPushed = 0;
|
|
8
14
|
constructor(maxSize = 2000) {
|
|
9
15
|
this.maxSize = maxSize;
|
|
16
|
+
this.buf = new Array(maxSize);
|
|
10
17
|
}
|
|
11
18
|
/**
|
|
12
|
-
* Add an entry to the buffer
|
|
13
|
-
* If buffer is full, oldest entry is
|
|
19
|
+
* Add an entry to the buffer — O(1) always.
|
|
20
|
+
* If buffer is full, the oldest entry is overwritten.
|
|
14
21
|
*/
|
|
15
22
|
push(entry) {
|
|
16
23
|
this._totalPushed++;
|
|
17
|
-
this.
|
|
18
|
-
|
|
19
|
-
this.
|
|
24
|
+
if (this.count < this.maxSize) {
|
|
25
|
+
// Buffer not yet full — append at head + count
|
|
26
|
+
this.buf[(this.head + this.count) % this.maxSize] = entry;
|
|
27
|
+
this.count++;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
// Overwrite oldest entry, advance head
|
|
31
|
+
this.buf[this.head] = entry;
|
|
32
|
+
this.head = (this.head + 1) % this.maxSize;
|
|
20
33
|
}
|
|
21
34
|
}
|
|
22
35
|
/**
|
|
23
|
-
* Get
|
|
36
|
+
* Get a single entry by logical index (0 = oldest). O(1).
|
|
37
|
+
* Returns undefined if index is out of range.
|
|
38
|
+
*/
|
|
39
|
+
get(index) {
|
|
40
|
+
if (index < 0 || index >= this.count)
|
|
41
|
+
return undefined;
|
|
42
|
+
return this.buf[(this.head + index) % this.maxSize];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Get a range of entries [start, end) by logical index. O(end - start).
|
|
46
|
+
* Clamps to valid range. Returns a new array.
|
|
47
|
+
*/
|
|
48
|
+
getRange(start, end) {
|
|
49
|
+
const s = Math.max(0, start);
|
|
50
|
+
const e = Math.min(this.count, end);
|
|
51
|
+
const result = [];
|
|
52
|
+
for (let i = s; i < e; i++) {
|
|
53
|
+
result.push(this.buf[(this.head + i) % this.maxSize]);
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Get all entries in insertion order (optionally filtered).
|
|
59
|
+
* Allocates a new array — used for full renders, not the hot path.
|
|
24
60
|
*/
|
|
25
61
|
getEntries(filter) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
62
|
+
const result = [];
|
|
63
|
+
for (let i = 0; i < this.count; i++) {
|
|
64
|
+
const entry = this.buf[(this.head + i) % this.maxSize];
|
|
65
|
+
if (!filter || filter(entry)) {
|
|
66
|
+
result.push(entry);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
29
70
|
}
|
|
30
71
|
/**
|
|
31
72
|
* Clear all entries
|
|
32
73
|
*/
|
|
33
74
|
clear() {
|
|
34
|
-
this.
|
|
75
|
+
this.buf = new Array(this.maxSize);
|
|
76
|
+
this.head = 0;
|
|
77
|
+
this.count = 0;
|
|
35
78
|
this._totalPushed = 0;
|
|
36
79
|
}
|
|
37
80
|
/**
|
|
38
81
|
* Get entry count
|
|
39
82
|
*/
|
|
40
83
|
get size() {
|
|
41
|
-
return this.
|
|
84
|
+
return this.count;
|
|
42
85
|
}
|
|
43
86
|
/**
|
|
44
87
|
* Get total entries ever pushed (including evicted ones)
|
|
@@ -50,7 +93,7 @@ export class LogBuffer {
|
|
|
50
93
|
* Get number of entries evicted due to buffer overflow
|
|
51
94
|
*/
|
|
52
95
|
get evicted() {
|
|
53
|
-
return this._totalPushed - this.
|
|
96
|
+
return this._totalPushed - this.count;
|
|
54
97
|
}
|
|
55
98
|
/**
|
|
56
99
|
* Get the maximum capacity
|
package/dist/eruda/loader.js
CHANGED
|
@@ -86,10 +86,12 @@ export async function loadEruda(options) {
|
|
|
86
86
|
const ggPlugin = createGgPlugin(options, gg);
|
|
87
87
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
88
88
|
eruda.add(ggPlugin);
|
|
89
|
-
//
|
|
89
|
+
// Select GG tab as default (but don't open the panel)
|
|
90
90
|
eruda.show('GG');
|
|
91
|
-
//
|
|
92
|
-
|
|
91
|
+
// Open the panel if requested
|
|
92
|
+
if (options.open) {
|
|
93
|
+
eruda.show();
|
|
94
|
+
}
|
|
93
95
|
// Run diagnostics after Eruda is ready so they appear in Console tab
|
|
94
96
|
await runGgDiagnostics();
|
|
95
97
|
}
|
package/dist/eruda/plugin.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DEV } from 'esm-env';
|
|
2
2
|
import { LogBuffer } from './buffer.js';
|
|
3
|
+
import { Virtualizer, elementScroll, observeElementOffset, observeElementRect, measureElement } from '@tanstack/virtual-core';
|
|
3
4
|
const _ggCallSitesPlugin = typeof __GG_TAG_PLUGIN__ !== 'undefined' ? __GG_TAG_PLUGIN__ : false;
|
|
4
5
|
/**
|
|
5
6
|
* Creates the gg Eruda plugin
|
|
@@ -19,8 +20,15 @@ export function createGgPlugin(options, gg) {
|
|
|
19
20
|
let filterExpanded = false;
|
|
20
21
|
let filterPattern = '';
|
|
21
22
|
const enabledNamespaces = new Set();
|
|
22
|
-
//
|
|
23
|
-
let
|
|
23
|
+
// Virtual scroll: filtered indices into the buffer (the "rows" the virtualizer sees)
|
|
24
|
+
let filteredIndices = [];
|
|
25
|
+
// Virtual scroll: the @tanstack/virtual-core Virtualizer instance
|
|
26
|
+
let virtualizer = null;
|
|
27
|
+
// Virtual scroll: track expanded state so it survives re-renders.
|
|
28
|
+
// Keys are the uniqueId strings (e.g. "42-0") for object details and
|
|
29
|
+
// stack IDs (e.g. "stack-42") for stack traces.
|
|
30
|
+
const expandedDetails = new Set();
|
|
31
|
+
const expandedStacks = new Set();
|
|
24
32
|
// Toast state for "namespace hidden" feedback
|
|
25
33
|
let lastHiddenPattern = null; // filterPattern before the hide (for undo)
|
|
26
34
|
let hasSeenToastExplanation = false; // first toast auto-expands help text
|
|
@@ -120,9 +128,9 @@ export function createGgPlugin(options, gg) {
|
|
|
120
128
|
const time = new Date(entry.timestamp).toISOString().slice(11, 23);
|
|
121
129
|
// Trim namespace and strip 'gg:' prefix to save tokens
|
|
122
130
|
const ns = entry.namespace.trim().replace(/^gg:/, '');
|
|
123
|
-
// Include expression
|
|
131
|
+
// Include expression on its own line above the value when toggle is enabled
|
|
124
132
|
const hasSrcExpr = !entry.level && entry.src?.trim() && !/^['"`]/.test(entry.src);
|
|
125
|
-
const
|
|
133
|
+
const exprLine = includeExpressions && hasSrcExpr ? `\u2039${entry.src}\u203A\n` : '';
|
|
126
134
|
// Format args: compact JSON for objects, primitives as-is
|
|
127
135
|
const argsStr = entry.args
|
|
128
136
|
.map((arg) => {
|
|
@@ -133,7 +141,7 @@ export function createGgPlugin(options, gg) {
|
|
|
133
141
|
return stripAnsi(String(arg));
|
|
134
142
|
})
|
|
135
143
|
.join(' ');
|
|
136
|
-
return `${time} ${ns} ${argsStr}
|
|
144
|
+
return `${exprLine}${time} ${ns} ${argsStr}`;
|
|
137
145
|
}
|
|
138
146
|
const plugin = {
|
|
139
147
|
name: 'GG',
|
|
@@ -225,8 +233,19 @@ export function createGgPlugin(options, gg) {
|
|
|
225
233
|
if (gg) {
|
|
226
234
|
gg._onLog = null;
|
|
227
235
|
}
|
|
236
|
+
// Clean up virtualizer
|
|
237
|
+
if (virtualizer && $el) {
|
|
238
|
+
const containerDom = $el.find('.gg-log-container').get(0);
|
|
239
|
+
if (containerDom) {
|
|
240
|
+
const cleanup = containerDom.__ggVirtualCleanup;
|
|
241
|
+
if (cleanup)
|
|
242
|
+
cleanup();
|
|
243
|
+
}
|
|
244
|
+
virtualizer = null;
|
|
245
|
+
}
|
|
228
246
|
buffer.clear();
|
|
229
247
|
allNamespacesSet.clear();
|
|
248
|
+
filteredIndices = [];
|
|
230
249
|
}
|
|
231
250
|
};
|
|
232
251
|
function toggleNamespace(namespace, enable) {
|
|
@@ -369,7 +388,10 @@ export function createGgPlugin(options, gg) {
|
|
|
369
388
|
function gridColumns() {
|
|
370
389
|
const ns = nsColWidth !== null ? `${nsColWidth}px` : 'auto';
|
|
371
390
|
// Grid columns: diff | ns | handle | content
|
|
372
|
-
|
|
391
|
+
// Diff uses a fixed width (3.5em) instead of auto to avoid column jitter
|
|
392
|
+
// when virtual scroll swaps rows in/out — only ~50 rows are in the DOM
|
|
393
|
+
// at a time so auto would resize based on visible subset.
|
|
394
|
+
return `3.5em ${ns} 4px 1fr`;
|
|
373
395
|
}
|
|
374
396
|
function buildHTML() {
|
|
375
397
|
return `
|
|
@@ -380,9 +402,11 @@ export function createGgPlugin(options, gg) {
|
|
|
380
402
|
column-gap: 0;
|
|
381
403
|
align-items: start !important;
|
|
382
404
|
}
|
|
383
|
-
/*
|
|
405
|
+
/* Virtual scroll: each entry is a subgrid row with measurable height */
|
|
384
406
|
.gg-log-entry {
|
|
385
|
-
display:
|
|
407
|
+
display: grid;
|
|
408
|
+
grid-template-columns: subgrid;
|
|
409
|
+
grid-column: 1 / -1;
|
|
386
410
|
}
|
|
387
411
|
.gg-log-header {
|
|
388
412
|
display: contents;
|
|
@@ -395,6 +419,11 @@ export function createGgPlugin(options, gg) {
|
|
|
395
419
|
align-self: start !important;
|
|
396
420
|
border-top: 1px solid rgba(0,0,0,0.05);
|
|
397
421
|
}
|
|
422
|
+
/* Virtual scroll: spacer provides total height for scrollbar */
|
|
423
|
+
.gg-virtual-spacer {
|
|
424
|
+
position: relative;
|
|
425
|
+
width: 100%;
|
|
426
|
+
}
|
|
398
427
|
.gg-reset-filter-btn:hover {
|
|
399
428
|
background: #1976D2 !important;
|
|
400
429
|
transform: translateY(-1px);
|
|
@@ -1659,7 +1688,20 @@ export function createGgPlugin(options, gg) {
|
|
|
1659
1688
|
return;
|
|
1660
1689
|
const details = containerEl.querySelector(`.gg-details[data-index="${index}"]`);
|
|
1661
1690
|
if (details) {
|
|
1662
|
-
|
|
1691
|
+
const nowVisible = details.style.display === 'none';
|
|
1692
|
+
details.style.display = nowVisible ? 'block' : 'none';
|
|
1693
|
+
// Track state so it survives virtual scroll re-renders
|
|
1694
|
+
if (nowVisible) {
|
|
1695
|
+
expandedDetails.add(index);
|
|
1696
|
+
}
|
|
1697
|
+
else {
|
|
1698
|
+
expandedDetails.delete(index);
|
|
1699
|
+
}
|
|
1700
|
+
// Re-measure the entry so virtualizer adjusts total height
|
|
1701
|
+
const entryEl = details.closest('.gg-log-entry');
|
|
1702
|
+
if (entryEl && virtualizer) {
|
|
1703
|
+
virtualizer.measureElement(entryEl);
|
|
1704
|
+
}
|
|
1663
1705
|
}
|
|
1664
1706
|
return;
|
|
1665
1707
|
}
|
|
@@ -1673,6 +1715,18 @@ export function createGgPlugin(options, gg) {
|
|
|
1673
1715
|
const isExpanded = stackEl.classList.contains('expanded');
|
|
1674
1716
|
stackEl.classList.toggle('expanded');
|
|
1675
1717
|
target.textContent = isExpanded ? '▶ stack' : '▼ stack';
|
|
1718
|
+
// Track state so it survives virtual scroll re-renders
|
|
1719
|
+
if (isExpanded) {
|
|
1720
|
+
expandedStacks.delete(stackId);
|
|
1721
|
+
}
|
|
1722
|
+
else {
|
|
1723
|
+
expandedStacks.add(stackId);
|
|
1724
|
+
}
|
|
1725
|
+
// Re-measure the entry so virtualizer adjusts total height
|
|
1726
|
+
const entryEl = stackEl.closest('.gg-log-entry');
|
|
1727
|
+
if (entryEl && virtualizer) {
|
|
1728
|
+
virtualizer.measureElement(entryEl);
|
|
1729
|
+
}
|
|
1676
1730
|
}
|
|
1677
1731
|
return;
|
|
1678
1732
|
}
|
|
@@ -1817,7 +1871,7 @@ export function createGgPlugin(options, gg) {
|
|
|
1817
1871
|
const entryIdx = entryEl?.getAttribute('data-entry');
|
|
1818
1872
|
if (entryIdx === null || entryIdx === undefined)
|
|
1819
1873
|
return;
|
|
1820
|
-
const entry =
|
|
1874
|
+
const entry = buffer.get(Number(entryIdx));
|
|
1821
1875
|
if (!entry)
|
|
1822
1876
|
return;
|
|
1823
1877
|
e.preventDefault();
|
|
@@ -1839,7 +1893,7 @@ export function createGgPlugin(options, gg) {
|
|
|
1839
1893
|
const argIdx = target.getAttribute('data-arg');
|
|
1840
1894
|
if (entryIdx === null || argIdx === null)
|
|
1841
1895
|
return;
|
|
1842
|
-
const entry =
|
|
1896
|
+
const entry = buffer.get(Number(entryIdx));
|
|
1843
1897
|
if (!entry)
|
|
1844
1898
|
return;
|
|
1845
1899
|
const arg = entry.args[Number(argIdx)];
|
|
@@ -1992,8 +2046,11 @@ export function createGgPlugin(options, gg) {
|
|
|
1992
2046
|
containerEl.addEventListener('pointerup', onPointerUp);
|
|
1993
2047
|
resizeAttached = true;
|
|
1994
2048
|
}
|
|
1995
|
-
/** Build the HTML string for a single log entry
|
|
1996
|
-
|
|
2049
|
+
/** Build the HTML string for a single log entry.
|
|
2050
|
+
* @param index Buffer index (used for data-entry, expand IDs, tooltip lookup)
|
|
2051
|
+
* @param virtualIndex Position in filteredIndices (used by virtualizer for measurement)
|
|
2052
|
+
*/
|
|
2053
|
+
function renderEntryHTML(entry, index, virtualIndex) {
|
|
1997
2054
|
const color = entry.color || '#0066cc';
|
|
1998
2055
|
const diff = `+${humanize(entry.diff)}`;
|
|
1999
2056
|
// Split namespace into clickable segments on multiple delimiters: : @ / - _
|
|
@@ -2065,16 +2122,19 @@ export function createGgPlugin(options, gg) {
|
|
|
2065
2122
|
const uniqueId = `${index}-${argIdx}`;
|
|
2066
2123
|
// Expression header inside expanded details
|
|
2067
2124
|
const srcHeader = srcExpr ? `<div class="gg-details-src">${srcExpr}</div>` : '';
|
|
2068
|
-
// Store details separately to render after the row
|
|
2069
|
-
|
|
2125
|
+
// Store details separately to render after the row.
|
|
2126
|
+
// Restore expanded state from expandedDetails set so it
|
|
2127
|
+
// survives virtual scroll re-renders.
|
|
2128
|
+
const detailsVisible = expandedDetails.has(uniqueId);
|
|
2129
|
+
detailsHTML += `<div class="gg-details" data-index="${uniqueId}" style="display: ${detailsVisible ? 'block' : 'none'}; margin: 4px 0 8px 0; padding: 8px; background: #f8f8f8; border-left: 3px solid ${color}; font-size: 11px; overflow-x: auto;">${srcHeader}<pre style="margin: 0;">${jsonStr}</pre></div>`;
|
|
2070
2130
|
// data-entry/data-arg for hover tooltip lookup, data-src for expression context
|
|
2071
2131
|
const srcAttr = srcExpr ? ` data-src="${srcExpr}"` : '';
|
|
2072
2132
|
const srcIcon = srcExpr ? `<span class="gg-src-icon">\uD83D\uDD0D</span>` : '';
|
|
2073
|
-
// Show expression
|
|
2074
|
-
const
|
|
2075
|
-
?
|
|
2133
|
+
// Show expression on its own line above the value when toggle is enabled
|
|
2134
|
+
const exprAbove = showExpressions && srcExpr
|
|
2135
|
+
? `<div class="gg-inline-expr">\u2039${srcExpr}\u203A</div>`
|
|
2076
2136
|
: '';
|
|
2077
|
-
return
|
|
2137
|
+
return `${exprAbove}<span style="color: #888; cursor: pointer; text-decoration: underline;" class="gg-expand" data-index="${uniqueId}" data-entry="${index}" data-arg="${argIdx}"${srcAttr}>${srcIcon}${preview}</span>`;
|
|
2078
2138
|
}
|
|
2079
2139
|
else {
|
|
2080
2140
|
// Parse ANSI codes first, then convert URLs to clickable links
|
|
@@ -2097,27 +2157,31 @@ export function createGgPlugin(options, gg) {
|
|
|
2097
2157
|
: entry.level === 'error'
|
|
2098
2158
|
? ' gg-level-error'
|
|
2099
2159
|
: '';
|
|
2100
|
-
// Stack trace toggle (for error/trace entries with captured stacks)
|
|
2160
|
+
// Stack trace toggle (for error/trace entries with captured stacks).
|
|
2161
|
+
// Restore expanded state from expandedStacks set so it survives
|
|
2162
|
+
// virtual scroll re-renders.
|
|
2101
2163
|
let stackHTML = '';
|
|
2102
2164
|
if (entry.stack) {
|
|
2103
2165
|
const stackId = `stack-${index}`;
|
|
2166
|
+
const stackExpanded = expandedStacks.has(stackId);
|
|
2104
2167
|
stackHTML =
|
|
2105
|
-
`<span class="gg-stack-toggle" data-stack-id="${stackId}"
|
|
2106
|
-
`<div class="gg-stack-content" data-stack-id="${stackId}">${escapeHtml(entry.stack)}</div>`;
|
|
2168
|
+
`<span class="gg-stack-toggle" data-stack-id="${stackId}">${stackExpanded ? '▼' : '▶'} stack</span>` +
|
|
2169
|
+
`<div class="gg-stack-content${stackExpanded ? ' expanded' : ''}" data-stack-id="${stackId}">${escapeHtml(entry.stack)}</div>`;
|
|
2107
2170
|
}
|
|
2108
2171
|
// Expression tooltip: skip table entries (tableData) -- expression is just gg.table(...) which isn't useful
|
|
2109
2172
|
const hasSrcExpr = !entry.level && !entry.tableData && entry.src?.trim() && !/^['"`]/.test(entry.src);
|
|
2110
|
-
// For primitives-only entries,
|
|
2111
|
-
const
|
|
2112
|
-
?
|
|
2173
|
+
// For primitives-only entries, show expression on its own line above the value when showExpressions is enabled
|
|
2174
|
+
const exprAboveForPrimitives = showExpressions && hasSrcExpr && !argsHTML.includes('gg-expand')
|
|
2175
|
+
? `<div class="gg-inline-expr">\u2039${escapeHtml(entry.src)}\u203A</div>`
|
|
2113
2176
|
: '';
|
|
2114
|
-
|
|
2177
|
+
const vindexAttr = virtualIndex !== undefined ? ` data-vindex="${virtualIndex}"` : '';
|
|
2178
|
+
return (`<div class="gg-log-entry${levelClass}" data-entry="${index}"${vindexAttr}>` +
|
|
2115
2179
|
`<div class="gg-log-header">` +
|
|
2116
2180
|
`<div class="gg-log-diff" style="color: ${color};"${fileAttr}${lineAttr}${colAttr}>${diff}</div>` +
|
|
2117
2181
|
`<div class="gg-log-ns" style="color: ${color};" data-namespace="${escapeHtml(entry.namespace)}"><span class="gg-ns-text">${nsHTML}</span><button class="gg-ns-hide" data-namespace="${escapeHtml(entry.namespace)}" title="Hide this namespace">\u00d7</button></div>` +
|
|
2118
2182
|
`<div class="gg-log-handle"></div>` +
|
|
2119
2183
|
`</div>` +
|
|
2120
|
-
`<div class="gg-log-content"${hasSrcExpr ? ` data-src="${escapeHtml(entry.src)}"` : ''}>${
|
|
2184
|
+
`<div class="gg-log-content"${hasSrcExpr ? ` data-src="${escapeHtml(entry.src)}"` : ''}>${exprAboveForPrimitives}${argsHTML}${stackHTML}</div>` +
|
|
2121
2185
|
detailsHTML +
|
|
2122
2186
|
`</div>`);
|
|
2123
2187
|
}
|
|
@@ -2146,21 +2210,129 @@ export function createGgPlugin(options, gg) {
|
|
|
2146
2210
|
if (!copyCountSpan.length)
|
|
2147
2211
|
return;
|
|
2148
2212
|
const allCount = buffer.size;
|
|
2149
|
-
const visibleCount =
|
|
2213
|
+
const visibleCount = filteredIndices.length;
|
|
2150
2214
|
const countText = visibleCount === allCount
|
|
2151
2215
|
? `Copy ${visibleCount} ${visibleCount === 1 ? 'entry' : 'entries'}`
|
|
2152
2216
|
: `Copy ${visibleCount} / ${allCount} ${visibleCount === 1 ? 'entry' : 'entries'}`;
|
|
2153
2217
|
copyCountSpan.html(countText);
|
|
2154
2218
|
}
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2219
|
+
// ─── Virtual scroll helpers ───────────────────────────────────────────
|
|
2220
|
+
/** Rebuild the filtered index list from the buffer. */
|
|
2221
|
+
function rebuildFilteredIndices() {
|
|
2222
|
+
filteredIndices = [];
|
|
2223
|
+
for (let i = 0; i < buffer.size; i++) {
|
|
2224
|
+
const entry = buffer.get(i);
|
|
2225
|
+
if (enabledNamespaces.has(entry.namespace)) {
|
|
2226
|
+
filteredIndices.push(i);
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2161
2229
|
}
|
|
2230
|
+
/** Track whether user is near bottom (for auto-scroll decisions). */
|
|
2231
|
+
let userNearBottom = true;
|
|
2162
2232
|
/**
|
|
2163
|
-
*
|
|
2233
|
+
* Render the visible virtual items into the DOM grid.
|
|
2234
|
+
* Called by the virtualizer's onChange and after new entries arrive.
|
|
2235
|
+
*/
|
|
2236
|
+
let isRendering = false;
|
|
2237
|
+
function renderVirtualItems() {
|
|
2238
|
+
// Guard against re-entrant calls (measureElement → onChange → renderVirtualItems)
|
|
2239
|
+
if (isRendering)
|
|
2240
|
+
return;
|
|
2241
|
+
if (!$el || !virtualizer)
|
|
2242
|
+
return;
|
|
2243
|
+
const containerDom = $el.find('.gg-log-container').get(0);
|
|
2244
|
+
if (!containerDom)
|
|
2245
|
+
return;
|
|
2246
|
+
const spacer = containerDom.querySelector('.gg-virtual-spacer');
|
|
2247
|
+
const grid = containerDom.querySelector('.gg-log-grid');
|
|
2248
|
+
if (!spacer || !grid)
|
|
2249
|
+
return;
|
|
2250
|
+
const items = virtualizer.getVirtualItems();
|
|
2251
|
+
if (items.length === 0) {
|
|
2252
|
+
grid.innerHTML = '';
|
|
2253
|
+
return;
|
|
2254
|
+
}
|
|
2255
|
+
// Set the spacer's height so the scrollbar reflects the full virtual list
|
|
2256
|
+
spacer.style.height = `${virtualizer.getTotalSize()}px`;
|
|
2257
|
+
// Position the grid at the start offset of the first visible item
|
|
2258
|
+
const startOffset = items[0].start;
|
|
2259
|
+
grid.style.transform = `translateY(${startOffset}px)`;
|
|
2260
|
+
// Build HTML only for visible items
|
|
2261
|
+
const html = items
|
|
2262
|
+
.map((item) => {
|
|
2263
|
+
const bufferIdx = filteredIndices[item.index];
|
|
2264
|
+
const entry = buffer.get(bufferIdx);
|
|
2265
|
+
if (!entry)
|
|
2266
|
+
return '';
|
|
2267
|
+
return renderEntryHTML(entry, bufferIdx, item.index);
|
|
2268
|
+
})
|
|
2269
|
+
.join('');
|
|
2270
|
+
grid.innerHTML = html;
|
|
2271
|
+
// After inserting HTML, measure each rendered entry so the virtualizer
|
|
2272
|
+
// learns actual heights (drives dynamic sizing).
|
|
2273
|
+
isRendering = true;
|
|
2274
|
+
try {
|
|
2275
|
+
const entryEls = grid.querySelectorAll('.gg-log-entry');
|
|
2276
|
+
entryEls.forEach((el) => {
|
|
2277
|
+
virtualizer.measureElement(el);
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
2280
|
+
finally {
|
|
2281
|
+
isRendering = false;
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
/**
|
|
2285
|
+
* Create or reconfigure the virtualizer for the current filteredIndices.
|
|
2286
|
+
* Call after filter changes or full rebuilds.
|
|
2287
|
+
*/
|
|
2288
|
+
function setupVirtualizer(scrollToBottom) {
|
|
2289
|
+
if (!$el)
|
|
2290
|
+
return;
|
|
2291
|
+
const containerDom = $el.find('.gg-log-container').get(0);
|
|
2292
|
+
if (!containerDom)
|
|
2293
|
+
return;
|
|
2294
|
+
// Tear down previous virtualizer
|
|
2295
|
+
if (virtualizer) {
|
|
2296
|
+
virtualizer.setOptions({
|
|
2297
|
+
...virtualizer.options,
|
|
2298
|
+
count: 0,
|
|
2299
|
+
enabled: false
|
|
2300
|
+
});
|
|
2301
|
+
virtualizer = null;
|
|
2302
|
+
}
|
|
2303
|
+
const count = filteredIndices.length;
|
|
2304
|
+
if (count === 0)
|
|
2305
|
+
return;
|
|
2306
|
+
virtualizer = new Virtualizer({
|
|
2307
|
+
count,
|
|
2308
|
+
getScrollElement: () => containerDom,
|
|
2309
|
+
estimateSize: () => 24, // estimated row height in px
|
|
2310
|
+
overscan: 10,
|
|
2311
|
+
observeElementRect,
|
|
2312
|
+
observeElementOffset,
|
|
2313
|
+
scrollToFn: elementScroll,
|
|
2314
|
+
measureElement: (el, entry, instance) => measureElement(el, entry, instance),
|
|
2315
|
+
// Use buffer index as the stable key for each virtual row
|
|
2316
|
+
getItemKey: (index) => filteredIndices[index],
|
|
2317
|
+
// The data-index attribute TanStack uses to find elements for measurement
|
|
2318
|
+
indexAttribute: 'data-vindex',
|
|
2319
|
+
onChange: () => {
|
|
2320
|
+
renderVirtualItems();
|
|
2321
|
+
}
|
|
2322
|
+
});
|
|
2323
|
+
// Mount the virtualizer (attaches scroll/resize observers)
|
|
2324
|
+
const cleanup = virtualizer._didMount();
|
|
2325
|
+
// Store cleanup for when we tear down (TODO: call on destroy)
|
|
2326
|
+
containerDom.__ggVirtualCleanup = cleanup;
|
|
2327
|
+
// Initial render, then scroll to bottom if requested
|
|
2328
|
+
virtualizer._willUpdate();
|
|
2329
|
+
renderVirtualItems();
|
|
2330
|
+
if (scrollToBottom) {
|
|
2331
|
+
virtualizer.scrollToIndex(count - 1, { align: 'end' });
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
/**
|
|
2335
|
+
* Incremental append: add new entries to the virtual scroll.
|
|
2164
2336
|
* Called from the rAF-batched _onLog path.
|
|
2165
2337
|
*/
|
|
2166
2338
|
function appendLogs(newEntries) {
|
|
@@ -2169,47 +2341,50 @@ export function createGgPlugin(options, gg) {
|
|
|
2169
2341
|
const logContainer = $el.find('.gg-log-container');
|
|
2170
2342
|
if (!logContainer.length)
|
|
2171
2343
|
return;
|
|
2172
|
-
// Filter new entries to only those matching enabled namespaces
|
|
2173
|
-
const visibleNew = newEntries.filter((e) => enabledNamespaces.has(e.namespace));
|
|
2174
|
-
// If there's no grid yet (first entries, or was showing empty state), do a full render
|
|
2175
2344
|
const containerDom = logContainer.get(0);
|
|
2176
|
-
|
|
2177
|
-
|
|
2345
|
+
if (!containerDom)
|
|
2346
|
+
return;
|
|
2347
|
+
// Check if we need a full render (no grid yet, or empty state showing)
|
|
2348
|
+
const grid = containerDom.querySelector('.gg-log-grid');
|
|
2349
|
+
if (!grid) {
|
|
2178
2350
|
renderLogs();
|
|
2179
2351
|
return;
|
|
2180
2352
|
}
|
|
2181
|
-
if
|
|
2182
|
-
|
|
2353
|
+
// Check if any new entries pass the filter
|
|
2354
|
+
const hasVisible = newEntries.some((e) => enabledNamespaces.has(e.namespace));
|
|
2355
|
+
if (!hasVisible && buffer.evicted === 0) {
|
|
2183
2356
|
updateCopyCount();
|
|
2184
2357
|
return;
|
|
2185
2358
|
}
|
|
2186
|
-
//
|
|
2187
|
-
|
|
2188
|
-
//
|
|
2189
|
-
|
|
2190
|
-
// Build HTML for just the new entries
|
|
2191
|
-
const html = visibleNew.map((entry, i) => renderEntryHTML(entry, startIndex + i)).join('');
|
|
2192
|
-
// Append to existing grid (no full DOM teardown!)
|
|
2193
|
-
grid.insertAdjacentHTML('beforeend', html);
|
|
2194
|
-
// Handle buffer overflow: the buffer evicts oldest entries when full.
|
|
2195
|
-
// Count DOM entries and remove excess from the front to stay in sync.
|
|
2196
|
-
// This avoids an expensive buffer.getEntries() + filter call.
|
|
2197
|
-
const domEntries = grid.querySelectorAll(':scope > .gg-log-entry');
|
|
2198
|
-
const excess = domEntries.length - buffer.capacity;
|
|
2199
|
-
if (excess > 0) {
|
|
2200
|
-
for (let i = 0; i < excess; i++) {
|
|
2201
|
-
// Remove the entry and any associated .gg-details siblings
|
|
2202
|
-
domEntries[i].remove();
|
|
2203
|
-
}
|
|
2204
|
-
// Trim renderedEntries from the front to match
|
|
2205
|
-
renderedEntries.splice(0, excess);
|
|
2206
|
-
}
|
|
2359
|
+
// Rebuild filteredIndices from scratch. This is O(buffer.size) with a
|
|
2360
|
+
// Set lookup per entry — ~0.1ms for 2000 entries. Always correct even
|
|
2361
|
+
// when the buffer wraps and old logical indices shift.
|
|
2362
|
+
rebuildFilteredIndices();
|
|
2207
2363
|
updateCopyCount();
|
|
2208
2364
|
updateTruncationBanner();
|
|
2209
|
-
//
|
|
2365
|
+
// Check if user is near bottom before we update the virtualizer
|
|
2366
|
+
const nearBottom = containerDom.scrollHeight - containerDom.scrollTop - containerDom.clientHeight < 50;
|
|
2367
|
+
userNearBottom = nearBottom;
|
|
2368
|
+
// Update virtualizer count and re-render
|
|
2369
|
+
if (virtualizer) {
|
|
2370
|
+
virtualizer.setOptions({
|
|
2371
|
+
...virtualizer.options,
|
|
2372
|
+
count: filteredIndices.length,
|
|
2373
|
+
getItemKey: (index) => filteredIndices[index]
|
|
2374
|
+
});
|
|
2375
|
+
virtualizer._willUpdate();
|
|
2376
|
+
// Render first so spacer height is updated, then scroll
|
|
2377
|
+
renderVirtualItems();
|
|
2378
|
+
if (userNearBottom) {
|
|
2379
|
+
virtualizer.scrollToIndex(filteredIndices.length - 1, { align: 'end' });
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
else {
|
|
2383
|
+
// First entries — set up the virtualizer
|
|
2384
|
+
setupVirtualizer(true);
|
|
2385
|
+
}
|
|
2386
|
+
// Re-wire expanders (idempotent — only attaches once)
|
|
2210
2387
|
wireUpExpanders();
|
|
2211
|
-
// Smart auto-scroll
|
|
2212
|
-
autoScroll(containerDom);
|
|
2213
2388
|
}
|
|
2214
2389
|
/** Full render: rebuild the entire log view (used for filter changes, clear, show, etc.) */
|
|
2215
2390
|
function renderLogs() {
|
|
@@ -2218,16 +2393,27 @@ export function createGgPlugin(options, gg) {
|
|
|
2218
2393
|
const logContainer = $el.find('.gg-log-container');
|
|
2219
2394
|
if (!logContainer.length)
|
|
2220
2395
|
return;
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2396
|
+
// Clear expansion state on full rebuild
|
|
2397
|
+
expandedDetails.clear();
|
|
2398
|
+
expandedStacks.clear();
|
|
2399
|
+
// Rebuild filtered indices from scratch
|
|
2400
|
+
rebuildFilteredIndices();
|
|
2225
2401
|
updateCopyCount();
|
|
2226
2402
|
updateTruncationBanner();
|
|
2227
|
-
if (
|
|
2228
|
-
|
|
2403
|
+
if (filteredIndices.length === 0) {
|
|
2404
|
+
// Tear down virtualizer
|
|
2405
|
+
if (virtualizer) {
|
|
2406
|
+
const containerDom = logContainer.get(0);
|
|
2407
|
+
if (containerDom) {
|
|
2408
|
+
const cleanup = containerDom.__ggVirtualCleanup;
|
|
2409
|
+
if (cleanup)
|
|
2410
|
+
cleanup();
|
|
2411
|
+
}
|
|
2412
|
+
virtualizer = null;
|
|
2413
|
+
}
|
|
2414
|
+
const hasFilteredLogs = buffer.size > 0;
|
|
2229
2415
|
const message = hasFilteredLogs
|
|
2230
|
-
? `All ${
|
|
2416
|
+
? `All ${buffer.size} logs filtered out.`
|
|
2231
2417
|
: 'No logs captured yet. Call gg() to see output here.';
|
|
2232
2418
|
const resetButton = hasFilteredLogs
|
|
2233
2419
|
? '<button class="gg-reset-filter-btn" style="margin-top: 12px; padding: 10px 20px; cursor: pointer; border: 1px solid #2196F3; background: #2196F3; color: white; border-radius: 6px; font-size: 13px; font-weight: 500; transition: background 0.2s;">Show all logs (gg:*)</button>'
|
|
@@ -2235,23 +2421,18 @@ export function createGgPlugin(options, gg) {
|
|
|
2235
2421
|
logContainer.html(`<div style="padding: 20px; text-align: center; opacity: 0.5;">${message}<div>${resetButton}</div></div>`);
|
|
2236
2422
|
return;
|
|
2237
2423
|
}
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
containerDom.appendChild(tip);
|
|
2248
|
-
}
|
|
2249
|
-
// Re-wire expanders after rendering
|
|
2424
|
+
// Build the virtual scroll DOM structure:
|
|
2425
|
+
// - .gg-virtual-spacer: sized to total virtual height (provides scrollbar)
|
|
2426
|
+
// - .gg-log-grid: positioned absolutely, translated to visible offset, holds only visible entries
|
|
2427
|
+
const gridClasses = `gg-log-grid${filterExpanded ? ' filter-mode' : ''}${showExpressions ? ' gg-show-expr' : ''}`;
|
|
2428
|
+
logContainer.html(`<div class="gg-virtual-spacer">` +
|
|
2429
|
+
`<div class="${gridClasses}" style="position: absolute; top: 0; left: 0; width: 100%; grid-template-columns: ${gridColumns()};"></div>` +
|
|
2430
|
+
`</div>` +
|
|
2431
|
+
`<div class="gg-hover-tooltip"></div>`);
|
|
2432
|
+
// Re-wire event delegation (idempotent)
|
|
2250
2433
|
wireUpExpanders();
|
|
2251
|
-
//
|
|
2252
|
-
|
|
2253
|
-
if (el)
|
|
2254
|
-
el.scrollTop = el.scrollHeight;
|
|
2434
|
+
// Create virtualizer and render
|
|
2435
|
+
setupVirtualizer(true);
|
|
2255
2436
|
}
|
|
2256
2437
|
/** Format ms like debug's `ms` package: 0ms, 500ms, 5s, 2m, 1h, 3d */
|
|
2257
2438
|
function humanize(ms) {
|
package/dist/eruda/types.d.ts
CHANGED
|
@@ -17,6 +17,11 @@ export interface GgErudaOptions {
|
|
|
17
17
|
* @default {}
|
|
18
18
|
*/
|
|
19
19
|
erudaOptions?: Record<string, unknown>;
|
|
20
|
+
/**
|
|
21
|
+
* Whether to open the GgConsole panel on load (not just the floating icon)
|
|
22
|
+
* @default false
|
|
23
|
+
*/
|
|
24
|
+
open?: boolean;
|
|
20
25
|
}
|
|
21
26
|
/** Log severity level */
|
|
22
27
|
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leftium/gg",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.46",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/Leftium/gg.git"
|
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
],
|
|
71
71
|
"dependencies": {
|
|
72
72
|
"@sveltejs/acorn-typescript": "^1.0.9",
|
|
73
|
+
"@tanstack/virtual-core": "^3.13.18",
|
|
73
74
|
"@types/estree": "^1.0.8",
|
|
74
75
|
"acorn": "^8.15.0",
|
|
75
76
|
"dotenv": "^17.2.4",
|