@leftium/gg 0.0.42 → 0.0.44
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/debug/common.js +4 -2
- package/dist/eruda/buffer.d.ts +13 -0
- package/dist/eruda/buffer.js +21 -0
- package/dist/eruda/loader.js +2 -0
- package/dist/eruda/plugin.js +286 -185
- package/dist/gg.js +12 -1
- package/package.json +1 -1
package/dist/debug/common.js
CHANGED
|
@@ -15,7 +15,9 @@ export function humanize(ms) {
|
|
|
15
15
|
return Math.round(ms / 60_000) + 'm';
|
|
16
16
|
if (abs >= 1_000)
|
|
17
17
|
return Math.round(ms / 1_000) + 's';
|
|
18
|
-
|
|
18
|
+
if (abs >= 1 || abs === 0)
|
|
19
|
+
return Math.round(ms) + 'ms';
|
|
20
|
+
return ms.toFixed(1) + 'ms';
|
|
19
21
|
}
|
|
20
22
|
/**
|
|
21
23
|
* Wildcard pattern matching (same algorithm as debug's `matchesTemplate`).
|
|
@@ -118,7 +120,7 @@ export function setup(env) {
|
|
|
118
120
|
const debug = function (...args) {
|
|
119
121
|
if (!debug.enabled)
|
|
120
122
|
return;
|
|
121
|
-
const curr =
|
|
123
|
+
const curr = performance.now();
|
|
122
124
|
const ms = curr - (prevTime || curr);
|
|
123
125
|
debug.diff = ms;
|
|
124
126
|
prevTime = curr;
|
package/dist/eruda/buffer.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { CapturedEntry } from './types.js';
|
|
|
5
5
|
export declare class LogBuffer {
|
|
6
6
|
private entries;
|
|
7
7
|
private maxSize;
|
|
8
|
+
private _totalPushed;
|
|
8
9
|
constructor(maxSize?: number);
|
|
9
10
|
/**
|
|
10
11
|
* Add an entry to the buffer
|
|
@@ -23,4 +24,16 @@ export declare class LogBuffer {
|
|
|
23
24
|
* Get entry count
|
|
24
25
|
*/
|
|
25
26
|
get size(): number;
|
|
27
|
+
/**
|
|
28
|
+
* Get total entries ever pushed (including evicted ones)
|
|
29
|
+
*/
|
|
30
|
+
get totalPushed(): number;
|
|
31
|
+
/**
|
|
32
|
+
* Get number of entries evicted due to buffer overflow
|
|
33
|
+
*/
|
|
34
|
+
get evicted(): number;
|
|
35
|
+
/**
|
|
36
|
+
* Get the maximum capacity
|
|
37
|
+
*/
|
|
38
|
+
get capacity(): number;
|
|
26
39
|
}
|
package/dist/eruda/buffer.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
export class LogBuffer {
|
|
5
5
|
entries = [];
|
|
6
6
|
maxSize;
|
|
7
|
+
_totalPushed = 0;
|
|
7
8
|
constructor(maxSize = 2000) {
|
|
8
9
|
this.maxSize = maxSize;
|
|
9
10
|
}
|
|
@@ -12,6 +13,7 @@ export class LogBuffer {
|
|
|
12
13
|
* If buffer is full, oldest entry is removed
|
|
13
14
|
*/
|
|
14
15
|
push(entry) {
|
|
16
|
+
this._totalPushed++;
|
|
15
17
|
this.entries.push(entry);
|
|
16
18
|
if (this.entries.length > this.maxSize) {
|
|
17
19
|
this.entries.shift(); // Remove oldest
|
|
@@ -30,6 +32,7 @@ export class LogBuffer {
|
|
|
30
32
|
*/
|
|
31
33
|
clear() {
|
|
32
34
|
this.entries = [];
|
|
35
|
+
this._totalPushed = 0;
|
|
33
36
|
}
|
|
34
37
|
/**
|
|
35
38
|
* Get entry count
|
|
@@ -37,4 +40,22 @@ export class LogBuffer {
|
|
|
37
40
|
get size() {
|
|
38
41
|
return this.entries.length;
|
|
39
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Get total entries ever pushed (including evicted ones)
|
|
45
|
+
*/
|
|
46
|
+
get totalPushed() {
|
|
47
|
+
return this._totalPushed;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Get number of entries evicted due to buffer overflow
|
|
51
|
+
*/
|
|
52
|
+
get evicted() {
|
|
53
|
+
return this._totalPushed - this.entries.length;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Get the maximum capacity
|
|
57
|
+
*/
|
|
58
|
+
get capacity() {
|
|
59
|
+
return this.maxSize;
|
|
60
|
+
}
|
|
40
61
|
}
|
package/dist/eruda/loader.js
CHANGED
|
@@ -88,6 +88,8 @@ export async function loadEruda(options) {
|
|
|
88
88
|
eruda.add(ggPlugin);
|
|
89
89
|
// Make GG tab the default selected tab
|
|
90
90
|
eruda.show('GG');
|
|
91
|
+
// Expand the Eruda panel (open from minimized floating button state)
|
|
92
|
+
eruda.show();
|
|
91
93
|
// Run diagnostics after Eruda is ready so they appear in Console tab
|
|
92
94
|
await runGgDiagnostics();
|
|
93
95
|
}
|
package/dist/eruda/plugin.js
CHANGED
|
@@ -37,6 +37,11 @@ export function createGgPlugin(options, gg) {
|
|
|
37
37
|
const COPY_FORMAT_KEY = 'gg-copy-format';
|
|
38
38
|
const URL_FORMAT_KEY = 'gg-url-format';
|
|
39
39
|
const PROJECT_ROOT_KEY = 'gg-project-root';
|
|
40
|
+
// Render batching: coalesce multiple _onLog calls into a single rAF
|
|
41
|
+
let renderPending = false;
|
|
42
|
+
let pendingEntries = []; // new entries since last render
|
|
43
|
+
// All namespaces ever seen (maintained incrementally, avoids scanning buffer)
|
|
44
|
+
const allNamespacesSet = new Set();
|
|
40
45
|
// Plugin detection state (probed once at init)
|
|
41
46
|
let openInEditorPluginDetected = null; // null = not yet probed
|
|
42
47
|
// Editor bins for launch-editor (common first, then alphabetical)
|
|
@@ -106,6 +111,30 @@ export function createGgPlugin(options, gg) {
|
|
|
106
111
|
localStorage.setItem(COPY_FORMAT_KEY, copyFormat);
|
|
107
112
|
}
|
|
108
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Format a single log entry for clipboard copy
|
|
116
|
+
* Produces: HH:MM:SS.mmm namespace args [optional expression]
|
|
117
|
+
*/
|
|
118
|
+
function formatEntryForClipboard(entry, includeExpressions) {
|
|
119
|
+
// Extract HH:MM:SS.mmm from timestamp (with milliseconds)
|
|
120
|
+
const time = new Date(entry.timestamp).toISOString().slice(11, 23);
|
|
121
|
+
// Trim namespace and strip 'gg:' prefix to save tokens
|
|
122
|
+
const ns = entry.namespace.trim().replace(/^gg:/, '');
|
|
123
|
+
// Include expression suffix when toggle is enabled
|
|
124
|
+
const hasSrcExpr = !entry.level && entry.src?.trim() && !/^['"`]/.test(entry.src);
|
|
125
|
+
const exprSuffix = includeExpressions && hasSrcExpr ? ` \u2039${entry.src}\u203A` : '';
|
|
126
|
+
// Format args: compact JSON for objects, primitives as-is
|
|
127
|
+
const argsStr = entry.args
|
|
128
|
+
.map((arg) => {
|
|
129
|
+
if (typeof arg === 'object' && arg !== null) {
|
|
130
|
+
return JSON.stringify(arg);
|
|
131
|
+
}
|
|
132
|
+
// Strip ANSI escape codes from string args
|
|
133
|
+
return stripAnsi(String(arg));
|
|
134
|
+
})
|
|
135
|
+
.join(' ');
|
|
136
|
+
return `${time} ${ns} ${argsStr}${exprSuffix}`;
|
|
137
|
+
}
|
|
109
138
|
const plugin = {
|
|
110
139
|
name: 'GG',
|
|
111
140
|
init($container) {
|
|
@@ -117,8 +146,9 @@ export function createGgPlugin(options, gg) {
|
|
|
117
146
|
// Register the capture hook on gg
|
|
118
147
|
if (gg) {
|
|
119
148
|
gg._onLog = (entry) => {
|
|
120
|
-
// Track namespaces
|
|
121
|
-
const
|
|
149
|
+
// Track namespaces incrementally (O(1) instead of scanning buffer)
|
|
150
|
+
const isNewNamespace = !allNamespacesSet.has(entry.namespace);
|
|
151
|
+
allNamespacesSet.add(entry.namespace);
|
|
122
152
|
buffer.push(entry);
|
|
123
153
|
// Add new namespace to enabledNamespaces if it matches the current pattern
|
|
124
154
|
const effectivePattern = filterPattern || 'gg:*';
|
|
@@ -126,10 +156,20 @@ export function createGgPlugin(options, gg) {
|
|
|
126
156
|
enabledNamespaces.add(entry.namespace);
|
|
127
157
|
}
|
|
128
158
|
// Update filter UI if new namespace appeared (updates button summary count)
|
|
129
|
-
if (
|
|
159
|
+
if (isNewNamespace) {
|
|
130
160
|
renderFilterUI();
|
|
131
161
|
}
|
|
132
|
-
|
|
162
|
+
// Batch: collect pending entries, schedule one render per frame
|
|
163
|
+
pendingEntries.push(entry);
|
|
164
|
+
if (!renderPending) {
|
|
165
|
+
renderPending = true;
|
|
166
|
+
requestAnimationFrame(() => {
|
|
167
|
+
renderPending = false;
|
|
168
|
+
const batch = pendingEntries;
|
|
169
|
+
pendingEntries = [];
|
|
170
|
+
appendLogs(batch);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
133
173
|
};
|
|
134
174
|
}
|
|
135
175
|
// Probe for openInEditorPlugin (status 222) and auto-populate $ROOT in dev mode
|
|
@@ -163,6 +203,11 @@ export function createGgPlugin(options, gg) {
|
|
|
163
203
|
wireUpFilterUI();
|
|
164
204
|
wireUpSettingsUI();
|
|
165
205
|
wireUpToast();
|
|
206
|
+
// Discard any entries queued during early-buffer replay (before the DOM
|
|
207
|
+
// existed). renderLogs() below will do a full render from buffer, so the
|
|
208
|
+
// pending rAF batch would only duplicate those entries.
|
|
209
|
+
pendingEntries = [];
|
|
210
|
+
renderPending = false;
|
|
166
211
|
renderLogs();
|
|
167
212
|
},
|
|
168
213
|
show() {
|
|
@@ -181,6 +226,7 @@ export function createGgPlugin(options, gg) {
|
|
|
181
226
|
gg._onLog = null;
|
|
182
227
|
}
|
|
183
228
|
buffer.clear();
|
|
229
|
+
allNamespacesSet.clear();
|
|
184
230
|
}
|
|
185
231
|
};
|
|
186
232
|
function toggleNamespace(namespace, enable) {
|
|
@@ -262,10 +308,7 @@ export function createGgPlugin(options, gg) {
|
|
|
262
308
|
return parts.join(',');
|
|
263
309
|
}
|
|
264
310
|
function getAllCapturedNamespaces() {
|
|
265
|
-
|
|
266
|
-
const nsSet = new Set();
|
|
267
|
-
entries.forEach((e) => nsSet.add(e.namespace));
|
|
268
|
-
return Array.from(nsSet).sort();
|
|
311
|
+
return Array.from(allNamespacesSet).sort();
|
|
269
312
|
}
|
|
270
313
|
function namespaceMatchesPattern(namespace, pattern) {
|
|
271
314
|
if (!pattern)
|
|
@@ -946,6 +989,7 @@ export function createGgPlugin(options, gg) {
|
|
|
946
989
|
</div>
|
|
947
990
|
<div class="gg-filter-panel"></div>
|
|
948
991
|
<div class="gg-settings-panel"></div>
|
|
992
|
+
<div class="gg-truncation-banner" style="display: none; padding: 6px 12px; background: #7f4f00; color: #ffe0a0; font-size: 11px; align-items: center; gap: 6px; flex-shrink: 0;"></div>
|
|
949
993
|
<div class="gg-log-container" style="flex: 1; overflow-y: auto; overflow-x: hidden; font-family: monospace; font-size: 12px; touch-action: pan-y; overscroll-behavior: contain;"></div>
|
|
950
994
|
<div class="gg-toast"></div>
|
|
951
995
|
<iframe class="gg-editor-iframe" hidden title="open-in-editor"></iframe>
|
|
@@ -1348,6 +1392,7 @@ export function createGgPlugin(options, gg) {
|
|
|
1348
1392
|
return;
|
|
1349
1393
|
$el.find('.gg-clear-btn').on('click', () => {
|
|
1350
1394
|
buffer.clear();
|
|
1395
|
+
allNamespacesSet.clear();
|
|
1351
1396
|
renderLogs();
|
|
1352
1397
|
});
|
|
1353
1398
|
$el.find('.gg-copy-btn').on('click', async () => {
|
|
@@ -1355,26 +1400,7 @@ export function createGgPlugin(options, gg) {
|
|
|
1355
1400
|
// Apply same filtering as renderLogs() - only copy visible entries
|
|
1356
1401
|
const entries = allEntries.filter((entry) => enabledNamespaces.has(entry.namespace));
|
|
1357
1402
|
const text = entries
|
|
1358
|
-
.map((e) =>
|
|
1359
|
-
// Extract just HH:MM:SS from timestamp (compact for LLMs)
|
|
1360
|
-
const time = new Date(e.timestamp).toISOString().slice(11, 19);
|
|
1361
|
-
// Trim namespace and strip 'gg:' prefix to save tokens
|
|
1362
|
-
const ns = e.namespace.trim().replace(/^gg:/, '');
|
|
1363
|
-
// Include expression suffix when toggle is enabled
|
|
1364
|
-
const hasSrcExpr = !e.level && e.src?.trim() && !/^['"`]/.test(e.src);
|
|
1365
|
-
const exprSuffix = showExpressions && hasSrcExpr ? ` \u2039${e.src}\u203A` : '';
|
|
1366
|
-
// Format args: compact JSON for objects, primitives as-is
|
|
1367
|
-
const argsStr = e.args
|
|
1368
|
-
.map((arg) => {
|
|
1369
|
-
if (typeof arg === 'object' && arg !== null) {
|
|
1370
|
-
return JSON.stringify(arg);
|
|
1371
|
-
}
|
|
1372
|
-
// Strip ANSI escape codes from string args
|
|
1373
|
-
return stripAnsi(String(arg));
|
|
1374
|
-
})
|
|
1375
|
-
.join(' ');
|
|
1376
|
-
return `${time} ${ns} ${argsStr}${exprSuffix}`;
|
|
1377
|
-
})
|
|
1403
|
+
.map((e) => formatEntryForClipboard(e, showExpressions))
|
|
1378
1404
|
.join('\n');
|
|
1379
1405
|
try {
|
|
1380
1406
|
await navigator.clipboard.writeText(text);
|
|
@@ -1795,20 +1821,7 @@ export function createGgPlugin(options, gg) {
|
|
|
1795
1821
|
if (!entry)
|
|
1796
1822
|
return;
|
|
1797
1823
|
e.preventDefault();
|
|
1798
|
-
const
|
|
1799
|
-
const ns = entry.namespace.trim().replace(/^gg:/, '');
|
|
1800
|
-
// Include expression suffix when toggle is enabled
|
|
1801
|
-
const hasSrcExpr = !entry.level && entry.src?.trim() && !/^['"`]/.test(entry.src);
|
|
1802
|
-
const exprSuffix = showExpressions && hasSrcExpr ? ` \u2039${entry.src}\u203A` : '';
|
|
1803
|
-
const argsStr = entry.args
|
|
1804
|
-
.map((arg) => {
|
|
1805
|
-
if (typeof arg === 'object' && arg !== null) {
|
|
1806
|
-
return JSON.stringify(arg);
|
|
1807
|
-
}
|
|
1808
|
-
return stripAnsi(String(arg));
|
|
1809
|
-
})
|
|
1810
|
-
.join(' ');
|
|
1811
|
-
const text = `${time} ${ns} ${argsStr}${exprSuffix}`;
|
|
1824
|
+
const text = formatEntryForClipboard(entry, showExpressions);
|
|
1812
1825
|
navigator.clipboard.writeText(text).then(() => {
|
|
1813
1826
|
showConfirmationTooltip(containerEl, contentEl, 'Copied message');
|
|
1814
1827
|
});
|
|
@@ -1979,21 +1992,238 @@ export function createGgPlugin(options, gg) {
|
|
|
1979
1992
|
containerEl.addEventListener('pointerup', onPointerUp);
|
|
1980
1993
|
resizeAttached = true;
|
|
1981
1994
|
}
|
|
1995
|
+
/** Build the HTML string for a single log entry */
|
|
1996
|
+
function renderEntryHTML(entry, index) {
|
|
1997
|
+
const color = entry.color || '#0066cc';
|
|
1998
|
+
const diff = `+${humanize(entry.diff)}`;
|
|
1999
|
+
// Split namespace into clickable segments on multiple delimiters: : @ / - _
|
|
2000
|
+
const parts = entry.namespace.split(/([:/@ \-_])/);
|
|
2001
|
+
const nsSegments = [];
|
|
2002
|
+
const delimiters = [];
|
|
2003
|
+
for (let i = 0; i < parts.length; i++) {
|
|
2004
|
+
if (i % 2 === 0) {
|
|
2005
|
+
// Even indices are segments
|
|
2006
|
+
if (parts[i])
|
|
2007
|
+
nsSegments.push(parts[i]);
|
|
2008
|
+
}
|
|
2009
|
+
else {
|
|
2010
|
+
// Odd indices are delimiters
|
|
2011
|
+
delimiters.push(parts[i]);
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
let nsHTML = '';
|
|
2015
|
+
for (let i = 0; i < nsSegments.length; i++) {
|
|
2016
|
+
const segment = escapeHtml(nsSegments[i]);
|
|
2017
|
+
// Build filter pattern: reconstruct namespace up to this point
|
|
2018
|
+
let filterPattern = '';
|
|
2019
|
+
for (let j = 0; j <= i; j++) {
|
|
2020
|
+
filterPattern += nsSegments[j];
|
|
2021
|
+
if (j < i) {
|
|
2022
|
+
filterPattern += delimiters[j];
|
|
2023
|
+
}
|
|
2024
|
+
else if (j < nsSegments.length - 1) {
|
|
2025
|
+
filterPattern += delimiters[j] + '*';
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
nsHTML += `<span class="gg-ns-segment" data-filter="${escapeHtml(filterPattern)}">${segment}</span>`;
|
|
2029
|
+
if (i < nsSegments.length - 1) {
|
|
2030
|
+
nsHTML += escapeHtml(delimiters[i]);
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
// Format each arg individually - objects are expandable
|
|
2034
|
+
let argsHTML = '';
|
|
2035
|
+
let detailsHTML = '';
|
|
2036
|
+
// Source expression for this entry (used in hover tooltips and expanded details)
|
|
2037
|
+
const srcExpr = entry.src?.trim() && !/^['"`]/.test(entry.src) ? escapeHtml(entry.src) : '';
|
|
2038
|
+
// HTML table rendering for gg.table() entries
|
|
2039
|
+
if (entry.tableData && entry.tableData.keys.length > 0) {
|
|
2040
|
+
const { keys, rows: tableRows } = entry.tableData;
|
|
2041
|
+
const headerCells = keys
|
|
2042
|
+
.map((k) => `<th style="padding: 2px 8px; border: 1px solid #ccc; background: #f0f0f0; font-size: 11px; white-space: nowrap;">${escapeHtml(k)}</th>`)
|
|
2043
|
+
.join('');
|
|
2044
|
+
const bodyRowsHtml = tableRows
|
|
2045
|
+
.map((row) => {
|
|
2046
|
+
const cells = keys
|
|
2047
|
+
.map((k) => {
|
|
2048
|
+
const val = row[k];
|
|
2049
|
+
const display = val === undefined ? '' : escapeHtml(String(val));
|
|
2050
|
+
return `<td style="padding: 2px 8px; border: 1px solid #ddd; font-size: 11px; white-space: nowrap;">${display}</td>`;
|
|
2051
|
+
})
|
|
2052
|
+
.join('');
|
|
2053
|
+
return `<tr>${cells}</tr>`;
|
|
2054
|
+
})
|
|
2055
|
+
.join('');
|
|
2056
|
+
argsHTML = `<div style="overflow-x: auto;"><table style="border-collapse: collapse; margin: 2px 0; font-family: monospace;"><thead><tr>${headerCells}</tr></thead><tbody>${bodyRowsHtml}</tbody></table></div>`;
|
|
2057
|
+
}
|
|
2058
|
+
else if (entry.args.length > 0) {
|
|
2059
|
+
argsHTML = entry.args
|
|
2060
|
+
.map((arg, argIdx) => {
|
|
2061
|
+
if (typeof arg === 'object' && arg !== null) {
|
|
2062
|
+
// Show expandable object
|
|
2063
|
+
const preview = Array.isArray(arg) ? `Array(${arg.length})` : 'Object';
|
|
2064
|
+
const jsonStr = escapeHtml(JSON.stringify(arg, null, 2));
|
|
2065
|
+
const uniqueId = `${index}-${argIdx}`;
|
|
2066
|
+
// Expression header inside expanded details
|
|
2067
|
+
const srcHeader = srcExpr ? `<div class="gg-details-src">${srcExpr}</div>` : '';
|
|
2068
|
+
// Store details separately to render after the row
|
|
2069
|
+
detailsHTML += `<div class="gg-details" data-index="${uniqueId}" style="display: none; margin: 4px 0 8px 0; padding: 8px; background: #f8f8f8; border-left: 3px solid ${color}; font-size: 11px; overflow-x: auto;">${srcHeader}<pre style="margin: 0;">${jsonStr}</pre></div>`;
|
|
2070
|
+
// data-entry/data-arg for hover tooltip lookup, data-src for expression context
|
|
2071
|
+
const srcAttr = srcExpr ? ` data-src="${srcExpr}"` : '';
|
|
2072
|
+
const srcIcon = srcExpr ? `<span class="gg-src-icon">\uD83D\uDD0D</span>` : '';
|
|
2073
|
+
// Show expression inline after preview when toggle is enabled
|
|
2074
|
+
const inlineExpr = showExpressions && srcExpr
|
|
2075
|
+
? ` <span class="gg-inline-expr">\u2039${srcExpr}\u203A</span>`
|
|
2076
|
+
: '';
|
|
2077
|
+
return `<span style="color: #888; cursor: pointer; text-decoration: underline;" class="gg-expand" data-index="${uniqueId}" data-entry="${index}" data-arg="${argIdx}"${srcAttr}>${srcIcon}${preview}${inlineExpr}</span>`;
|
|
2078
|
+
}
|
|
2079
|
+
else {
|
|
2080
|
+
// Parse ANSI codes first, then convert URLs to clickable links
|
|
2081
|
+
const argStr = String(arg);
|
|
2082
|
+
const parsedAnsi = parseAnsiToHtml(argStr);
|
|
2083
|
+
return `<span>${parsedAnsi}</span>`;
|
|
2084
|
+
}
|
|
2085
|
+
})
|
|
2086
|
+
.join(' ');
|
|
2087
|
+
}
|
|
2088
|
+
// Open-in-editor data attributes (file, line, col)
|
|
2089
|
+
const fileAttr = entry.file ? ` data-file="${escapeHtml(entry.file)}"` : '';
|
|
2090
|
+
const lineAttr = entry.line ? ` data-line="${entry.line}"` : '';
|
|
2091
|
+
const colAttr = entry.col ? ` data-col="${entry.col}"` : '';
|
|
2092
|
+
// Level class for info/warn/error styling
|
|
2093
|
+
const levelClass = entry.level === 'info'
|
|
2094
|
+
? ' gg-level-info'
|
|
2095
|
+
: entry.level === 'warn'
|
|
2096
|
+
? ' gg-level-warn'
|
|
2097
|
+
: entry.level === 'error'
|
|
2098
|
+
? ' gg-level-error'
|
|
2099
|
+
: '';
|
|
2100
|
+
// Stack trace toggle (for error/trace entries with captured stacks)
|
|
2101
|
+
let stackHTML = '';
|
|
2102
|
+
if (entry.stack) {
|
|
2103
|
+
const stackId = `stack-${index}`;
|
|
2104
|
+
stackHTML =
|
|
2105
|
+
`<span class="gg-stack-toggle" data-stack-id="${stackId}">▶ stack</span>` +
|
|
2106
|
+
`<div class="gg-stack-content" data-stack-id="${stackId}">${escapeHtml(entry.stack)}</div>`;
|
|
2107
|
+
}
|
|
2108
|
+
// Expression tooltip: skip table entries (tableData) -- expression is just gg.table(...) which isn't useful
|
|
2109
|
+
const hasSrcExpr = !entry.level && !entry.tableData && entry.src?.trim() && !/^['"`]/.test(entry.src);
|
|
2110
|
+
// For primitives-only entries, append inline expression when showExpressions is enabled
|
|
2111
|
+
const inlineExprForPrimitives = showExpressions && hasSrcExpr && !argsHTML.includes('gg-expand')
|
|
2112
|
+
? ` <span class="gg-inline-expr">\u2039${escapeHtml(entry.src)}\u203A</span>`
|
|
2113
|
+
: '';
|
|
2114
|
+
return (`<div class="gg-log-entry${levelClass}" data-entry="${index}">` +
|
|
2115
|
+
`<div class="gg-log-header">` +
|
|
2116
|
+
`<div class="gg-log-diff" style="color: ${color};"${fileAttr}${lineAttr}${colAttr}>${diff}</div>` +
|
|
2117
|
+
`<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
|
+
`<div class="gg-log-handle"></div>` +
|
|
2119
|
+
`</div>` +
|
|
2120
|
+
`<div class="gg-log-content"${hasSrcExpr ? ` data-src="${escapeHtml(entry.src)}"` : ''}>${argsHTML}${inlineExprForPrimitives}${stackHTML}</div>` +
|
|
2121
|
+
detailsHTML +
|
|
2122
|
+
`</div>`);
|
|
2123
|
+
}
|
|
2124
|
+
/** Update the copy-button count text */
|
|
2125
|
+
function updateTruncationBanner() {
|
|
2126
|
+
if (!$el)
|
|
2127
|
+
return;
|
|
2128
|
+
const banner = $el.find('.gg-truncation-banner').get(0);
|
|
2129
|
+
if (!banner)
|
|
2130
|
+
return;
|
|
2131
|
+
const evicted = buffer.evicted;
|
|
2132
|
+
if (evicted > 0) {
|
|
2133
|
+
const total = buffer.totalPushed;
|
|
2134
|
+
const retained = buffer.size;
|
|
2135
|
+
banner.innerHTML = `⚠ Showing ${retained.toLocaleString()} of ${total.toLocaleString()} messages — ${evicted.toLocaleString()} truncated. Increase <code style="font-family:monospace;background:rgba(255,255,255,0.15);padding:0 3px;border-radius:3px;">maxEntries</code> to retain more.`;
|
|
2136
|
+
banner.style.display = 'flex';
|
|
2137
|
+
}
|
|
2138
|
+
else {
|
|
2139
|
+
banner.style.display = 'none';
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
function updateCopyCount() {
|
|
2143
|
+
if (!$el)
|
|
2144
|
+
return;
|
|
2145
|
+
const copyCountSpan = $el.find('.gg-copy-count');
|
|
2146
|
+
if (!copyCountSpan.length)
|
|
2147
|
+
return;
|
|
2148
|
+
const allCount = buffer.size;
|
|
2149
|
+
const visibleCount = renderedEntries.length;
|
|
2150
|
+
const countText = visibleCount === allCount
|
|
2151
|
+
? `Copy ${visibleCount} ${visibleCount === 1 ? 'entry' : 'entries'}`
|
|
2152
|
+
: `Copy ${visibleCount} / ${allCount} ${visibleCount === 1 ? 'entry' : 'entries'}`;
|
|
2153
|
+
copyCountSpan.html(countText);
|
|
2154
|
+
}
|
|
2155
|
+
/** Scroll to bottom only if user is already near the bottom */
|
|
2156
|
+
function autoScroll(el) {
|
|
2157
|
+
const threshold = 50; // px from bottom
|
|
2158
|
+
const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < threshold;
|
|
2159
|
+
if (nearBottom)
|
|
2160
|
+
el.scrollTop = el.scrollHeight;
|
|
2161
|
+
}
|
|
2162
|
+
/**
|
|
2163
|
+
* Incremental append: add new entries to the existing DOM without full rebuild.
|
|
2164
|
+
* Called from the rAF-batched _onLog path.
|
|
2165
|
+
*/
|
|
2166
|
+
function appendLogs(newEntries) {
|
|
2167
|
+
if (!$el)
|
|
2168
|
+
return;
|
|
2169
|
+
const logContainer = $el.find('.gg-log-container');
|
|
2170
|
+
if (!logContainer.length)
|
|
2171
|
+
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
|
+
const containerDom = logContainer.get(0);
|
|
2176
|
+
const grid = containerDom?.querySelector('.gg-log-grid');
|
|
2177
|
+
if (!grid || !containerDom) {
|
|
2178
|
+
renderLogs();
|
|
2179
|
+
return;
|
|
2180
|
+
}
|
|
2181
|
+
if (visibleNew.length === 0) {
|
|
2182
|
+
// New entries were all filtered out, just update count
|
|
2183
|
+
updateCopyCount();
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
// Compute the starting index (position in the renderedEntries array)
|
|
2187
|
+
const startIndex = renderedEntries.length;
|
|
2188
|
+
// Add to renderedEntries
|
|
2189
|
+
renderedEntries.push(...visibleNew);
|
|
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
|
+
}
|
|
2207
|
+
updateCopyCount();
|
|
2208
|
+
updateTruncationBanner();
|
|
2209
|
+
// Re-wire expanders after rendering
|
|
2210
|
+
wireUpExpanders();
|
|
2211
|
+
// Smart auto-scroll
|
|
2212
|
+
autoScroll(containerDom);
|
|
2213
|
+
}
|
|
2214
|
+
/** Full render: rebuild the entire log view (used for filter changes, clear, show, etc.) */
|
|
1982
2215
|
function renderLogs() {
|
|
1983
2216
|
if (!$el)
|
|
1984
2217
|
return;
|
|
1985
2218
|
const logContainer = $el.find('.gg-log-container');
|
|
1986
|
-
|
|
1987
|
-
if (!logContainer.length || !copyCountSpan.length)
|
|
2219
|
+
if (!logContainer.length)
|
|
1988
2220
|
return;
|
|
1989
2221
|
const allEntries = buffer.getEntries();
|
|
1990
2222
|
// Apply filtering
|
|
1991
2223
|
const entries = allEntries.filter((entry) => enabledNamespaces.has(entry.namespace));
|
|
1992
2224
|
renderedEntries = entries;
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
: `Copy ${entries.length} / ${allEntries.length} ${entries.length === 1 ? 'entry' : 'entries'}`;
|
|
1996
|
-
copyCountSpan.html(countText);
|
|
2225
|
+
updateCopyCount();
|
|
2226
|
+
updateTruncationBanner();
|
|
1997
2227
|
if (entries.length === 0) {
|
|
1998
2228
|
const hasFilteredLogs = allEntries.length > 0;
|
|
1999
2229
|
const message = hasFilteredLogs
|
|
@@ -2006,139 +2236,7 @@ export function createGgPlugin(options, gg) {
|
|
|
2006
2236
|
return;
|
|
2007
2237
|
}
|
|
2008
2238
|
const logsHTML = `<div class="gg-log-grid${filterExpanded ? ' filter-mode' : ''}${showExpressions ? ' gg-show-expr' : ''}" style="grid-template-columns: ${gridColumns()};">${entries
|
|
2009
|
-
.map((entry, index) =>
|
|
2010
|
-
const color = entry.color || '#0066cc';
|
|
2011
|
-
const diff = `+${humanize(entry.diff)}`;
|
|
2012
|
-
// Split namespace into clickable segments on multiple delimiters: : @ / - _
|
|
2013
|
-
const parts = entry.namespace.split(/([:/@ \-_])/);
|
|
2014
|
-
const nsSegments = [];
|
|
2015
|
-
const delimiters = [];
|
|
2016
|
-
for (let i = 0; i < parts.length; i++) {
|
|
2017
|
-
if (i % 2 === 0) {
|
|
2018
|
-
// Even indices are segments
|
|
2019
|
-
if (parts[i])
|
|
2020
|
-
nsSegments.push(parts[i]);
|
|
2021
|
-
}
|
|
2022
|
-
else {
|
|
2023
|
-
// Odd indices are delimiters
|
|
2024
|
-
delimiters.push(parts[i]);
|
|
2025
|
-
}
|
|
2026
|
-
}
|
|
2027
|
-
let nsHTML = '';
|
|
2028
|
-
for (let i = 0; i < nsSegments.length; i++) {
|
|
2029
|
-
const segment = escapeHtml(nsSegments[i]);
|
|
2030
|
-
// Build filter pattern: reconstruct namespace up to this point
|
|
2031
|
-
let filterPattern = '';
|
|
2032
|
-
for (let j = 0; j <= i; j++) {
|
|
2033
|
-
filterPattern += nsSegments[j];
|
|
2034
|
-
if (j < i) {
|
|
2035
|
-
filterPattern += delimiters[j];
|
|
2036
|
-
}
|
|
2037
|
-
else if (j < nsSegments.length - 1) {
|
|
2038
|
-
filterPattern += delimiters[j] + '*';
|
|
2039
|
-
}
|
|
2040
|
-
}
|
|
2041
|
-
nsHTML += `<span class="gg-ns-segment" data-filter="${escapeHtml(filterPattern)}">${segment}</span>`;
|
|
2042
|
-
if (i < nsSegments.length - 1) {
|
|
2043
|
-
nsHTML += escapeHtml(delimiters[i]);
|
|
2044
|
-
}
|
|
2045
|
-
}
|
|
2046
|
-
// Format each arg individually - objects are expandable
|
|
2047
|
-
let argsHTML = '';
|
|
2048
|
-
let detailsHTML = '';
|
|
2049
|
-
// Source expression for this entry (used in hover tooltips and expanded details)
|
|
2050
|
-
const srcExpr = entry.src?.trim() && !/^['"`]/.test(entry.src) ? escapeHtml(entry.src) : '';
|
|
2051
|
-
// HTML table rendering for gg.table() entries
|
|
2052
|
-
if (entry.tableData && entry.tableData.keys.length > 0) {
|
|
2053
|
-
const { keys, rows: tableRows } = entry.tableData;
|
|
2054
|
-
const headerCells = keys
|
|
2055
|
-
.map((k) => `<th style="padding: 2px 8px; border: 1px solid #ccc; background: #f0f0f0; font-size: 11px; white-space: nowrap;">${escapeHtml(k)}</th>`)
|
|
2056
|
-
.join('');
|
|
2057
|
-
const bodyRowsHtml = tableRows
|
|
2058
|
-
.map((row) => {
|
|
2059
|
-
const cells = keys
|
|
2060
|
-
.map((k) => {
|
|
2061
|
-
const val = row[k];
|
|
2062
|
-
const display = val === undefined ? '' : escapeHtml(String(val));
|
|
2063
|
-
return `<td style="padding: 2px 8px; border: 1px solid #ddd; font-size: 11px; white-space: nowrap;">${display}</td>`;
|
|
2064
|
-
})
|
|
2065
|
-
.join('');
|
|
2066
|
-
return `<tr>${cells}</tr>`;
|
|
2067
|
-
})
|
|
2068
|
-
.join('');
|
|
2069
|
-
argsHTML = `<div style="overflow-x: auto;"><table style="border-collapse: collapse; margin: 2px 0; font-family: monospace;"><thead><tr>${headerCells}</tr></thead><tbody>${bodyRowsHtml}</tbody></table></div>`;
|
|
2070
|
-
}
|
|
2071
|
-
else if (entry.args.length > 0) {
|
|
2072
|
-
argsHTML = entry.args
|
|
2073
|
-
.map((arg, argIdx) => {
|
|
2074
|
-
if (typeof arg === 'object' && arg !== null) {
|
|
2075
|
-
// Show expandable object
|
|
2076
|
-
const preview = Array.isArray(arg) ? `Array(${arg.length})` : 'Object';
|
|
2077
|
-
const jsonStr = escapeHtml(JSON.stringify(arg, null, 2));
|
|
2078
|
-
const uniqueId = `${index}-${argIdx}`;
|
|
2079
|
-
// Expression header inside expanded details
|
|
2080
|
-
const srcHeader = srcExpr ? `<div class="gg-details-src">${srcExpr}</div>` : '';
|
|
2081
|
-
// Store details separately to render after the row
|
|
2082
|
-
detailsHTML += `<div class="gg-details" data-index="${uniqueId}" style="display: none; margin: 4px 0 8px 0; padding: 8px; background: #f8f8f8; border-left: 3px solid ${color}; font-size: 11px; overflow-x: auto;">${srcHeader}<pre style="margin: 0;">${jsonStr}</pre></div>`;
|
|
2083
|
-
// data-entry/data-arg for hover tooltip lookup, data-src for expression context
|
|
2084
|
-
const srcAttr = srcExpr ? ` data-src="${srcExpr}"` : '';
|
|
2085
|
-
const srcIcon = srcExpr ? `<span class="gg-src-icon">\uD83D\uDD0D</span>` : '';
|
|
2086
|
-
// Show expression inline after preview when toggle is enabled
|
|
2087
|
-
const inlineExpr = showExpressions && srcExpr
|
|
2088
|
-
? ` <span class="gg-inline-expr">\u2039${srcExpr}\u203A</span>`
|
|
2089
|
-
: '';
|
|
2090
|
-
return `<span style="color: #888; cursor: pointer; text-decoration: underline;" class="gg-expand" data-index="${uniqueId}" data-entry="${index}" data-arg="${argIdx}"${srcAttr}>${srcIcon}${preview}${inlineExpr}</span>`;
|
|
2091
|
-
}
|
|
2092
|
-
else {
|
|
2093
|
-
// Parse ANSI codes first, then convert URLs to clickable links
|
|
2094
|
-
const argStr = String(arg);
|
|
2095
|
-
const parsedAnsi = parseAnsiToHtml(argStr);
|
|
2096
|
-
// Note: URL linking happens after ANSI parsing, so links work inside colored text
|
|
2097
|
-
// This is a simple approach - URLs inside ANSI codes won't be linkified
|
|
2098
|
-
// For more complex parsing, we'd need to track ANSI state while matching URLs
|
|
2099
|
-
return `<span>${parsedAnsi}</span>`;
|
|
2100
|
-
}
|
|
2101
|
-
})
|
|
2102
|
-
.join(' ');
|
|
2103
|
-
}
|
|
2104
|
-
// Time diff will be clickable for open-in-editor when file metadata exists
|
|
2105
|
-
// Open-in-editor data attributes (file, line, col)
|
|
2106
|
-
const fileAttr = entry.file ? ` data-file="${escapeHtml(entry.file)}"` : '';
|
|
2107
|
-
const lineAttr = entry.line ? ` data-line="${entry.line}"` : '';
|
|
2108
|
-
const colAttr = entry.col ? ` data-col="${entry.col}"` : '';
|
|
2109
|
-
// Level class for info/warn/error styling
|
|
2110
|
-
const levelClass = entry.level === 'info'
|
|
2111
|
-
? ' gg-level-info'
|
|
2112
|
-
: entry.level === 'warn'
|
|
2113
|
-
? ' gg-level-warn'
|
|
2114
|
-
: entry.level === 'error'
|
|
2115
|
-
? ' gg-level-error'
|
|
2116
|
-
: '';
|
|
2117
|
-
// Stack trace toggle (for error/trace entries with captured stacks)
|
|
2118
|
-
let stackHTML = '';
|
|
2119
|
-
if (entry.stack) {
|
|
2120
|
-
const stackId = `stack-${index}`;
|
|
2121
|
-
stackHTML =
|
|
2122
|
-
`<span class="gg-stack-toggle" data-stack-id="${stackId}">▶ stack</span>` +
|
|
2123
|
-
`<div class="gg-stack-content" data-stack-id="${stackId}">${escapeHtml(entry.stack)}</div>`;
|
|
2124
|
-
}
|
|
2125
|
-
// Desktop: grid layout, Mobile: stacked layout
|
|
2126
|
-
// Expression tooltip: skip table entries (tableData) -- expression is just gg.table(...) which isn't useful
|
|
2127
|
-
const hasSrcExpr = !entry.level && !entry.tableData && entry.src?.trim() && !/^['"`]/.test(entry.src);
|
|
2128
|
-
// For primitives-only entries, append inline expression when showExpressions is enabled
|
|
2129
|
-
const inlineExprForPrimitives = showExpressions && hasSrcExpr && !argsHTML.includes('gg-expand')
|
|
2130
|
-
? ` <span class="gg-inline-expr">\u2039${escapeHtml(entry.src)}\u203A</span>`
|
|
2131
|
-
: '';
|
|
2132
|
-
return (`<div class="gg-log-entry${levelClass}" data-entry="${index}">` +
|
|
2133
|
-
`<div class="gg-log-header">` +
|
|
2134
|
-
`<div class="gg-log-diff" style="color: ${color};"${fileAttr}${lineAttr}${colAttr}>${diff}</div>` +
|
|
2135
|
-
`<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>` +
|
|
2136
|
-
`<div class="gg-log-handle"></div>` +
|
|
2137
|
-
`</div>` +
|
|
2138
|
-
`<div class="gg-log-content"${hasSrcExpr ? ` data-src="${escapeHtml(entry.src)}"` : ''}>${argsHTML}${inlineExprForPrimitives}${stackHTML}</div>` +
|
|
2139
|
-
detailsHTML +
|
|
2140
|
-
`</div>`);
|
|
2141
|
-
})
|
|
2239
|
+
.map((entry, index) => renderEntryHTML(entry, index))
|
|
2142
2240
|
.join('')}</div>`;
|
|
2143
2241
|
logContainer.html(logsHTML);
|
|
2144
2242
|
// Append hover tooltip div (destroyed by .html() each render, so re-create)
|
|
@@ -2166,12 +2264,15 @@ export function createGgPlugin(options, gg) {
|
|
|
2166
2264
|
return Math.round(ms / 60000) + 'm ';
|
|
2167
2265
|
if (abs >= 1000)
|
|
2168
2266
|
return Math.round(ms / 1000) + 's ';
|
|
2169
|
-
return ms + 'ms';
|
|
2267
|
+
return Math.round(ms) + 'ms';
|
|
2170
2268
|
}
|
|
2171
2269
|
function escapeHtml(text) {
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2270
|
+
return text
|
|
2271
|
+
.replace(/&/g, '&')
|
|
2272
|
+
.replace(/</g, '<')
|
|
2273
|
+
.replace(/>/g, '>')
|
|
2274
|
+
.replace(/"/g, '"')
|
|
2275
|
+
.replace(/'/g, ''');
|
|
2175
2276
|
}
|
|
2176
2277
|
/**
|
|
2177
2278
|
* Strip ANSI escape codes from text
|
package/dist/gg.js
CHANGED
|
@@ -182,6 +182,9 @@ const srcRootRegex = new RegExp(ggConfig.srcRootPattern, 'i');
|
|
|
182
182
|
// - Cache and reuse the same log function for a given callpoint.
|
|
183
183
|
const namespaceToLogFunction = new Map();
|
|
184
184
|
let maxCallpointLength = 0;
|
|
185
|
+
// Per-namespace prevTime for diff tracking (independent of debug library's enabled state,
|
|
186
|
+
// so GgConsole diffs are correct even when localStorage.debug doesn't include gg:*).
|
|
187
|
+
const namespaceToPrevTime = new Map();
|
|
185
188
|
// Cache: raw stack line → word tuple (avoids re-hashing the same call site)
|
|
186
189
|
const stackLineCache = new Map();
|
|
187
190
|
/**
|
|
@@ -311,6 +314,14 @@ function ggLog(options, ...args) {
|
|
|
311
314
|
else if (level === 'error') {
|
|
312
315
|
logArgs[0] = `⛔ ${logArgs[0]}`;
|
|
313
316
|
}
|
|
317
|
+
// Compute diff independently of the debug library's enabled state.
|
|
318
|
+
// ggLogFunction.diff only updates when the debugger is enabled (i.e. localStorage.debug
|
|
319
|
+
// matches the namespace), so relying on it would always show +0ms when console output is
|
|
320
|
+
// disabled — even though the GgConsole panel always captures entries.
|
|
321
|
+
const now = performance.now();
|
|
322
|
+
const prevTime = namespaceToPrevTime.get(namespace);
|
|
323
|
+
const diff = prevTime !== undefined ? now - prevTime : 0;
|
|
324
|
+
namespaceToPrevTime.set(namespace, now);
|
|
314
325
|
// Log to console via debug
|
|
315
326
|
if (logArgs.length === 1) {
|
|
316
327
|
ggLogFunction(logArgs[0]);
|
|
@@ -322,7 +333,7 @@ function ggLog(options, ...args) {
|
|
|
322
333
|
const entry = {
|
|
323
334
|
namespace,
|
|
324
335
|
color: ggLogFunction.color,
|
|
325
|
-
diff
|
|
336
|
+
diff,
|
|
326
337
|
message: logArgs.length === 1 ? String(logArgs[0]) : logArgs.map(String).join(' '),
|
|
327
338
|
args: logArgs,
|
|
328
339
|
timestamp: Date.now(),
|