@leftium/gg 0.0.43 → 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 +260 -151
- 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)
|
|
@@ -141,8 +146,9 @@ export function createGgPlugin(options, gg) {
|
|
|
141
146
|
// Register the capture hook on gg
|
|
142
147
|
if (gg) {
|
|
143
148
|
gg._onLog = (entry) => {
|
|
144
|
-
// Track namespaces
|
|
145
|
-
const
|
|
149
|
+
// Track namespaces incrementally (O(1) instead of scanning buffer)
|
|
150
|
+
const isNewNamespace = !allNamespacesSet.has(entry.namespace);
|
|
151
|
+
allNamespacesSet.add(entry.namespace);
|
|
146
152
|
buffer.push(entry);
|
|
147
153
|
// Add new namespace to enabledNamespaces if it matches the current pattern
|
|
148
154
|
const effectivePattern = filterPattern || 'gg:*';
|
|
@@ -150,10 +156,20 @@ export function createGgPlugin(options, gg) {
|
|
|
150
156
|
enabledNamespaces.add(entry.namespace);
|
|
151
157
|
}
|
|
152
158
|
// Update filter UI if new namespace appeared (updates button summary count)
|
|
153
|
-
if (
|
|
159
|
+
if (isNewNamespace) {
|
|
154
160
|
renderFilterUI();
|
|
155
161
|
}
|
|
156
|
-
|
|
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
|
+
}
|
|
157
173
|
};
|
|
158
174
|
}
|
|
159
175
|
// Probe for openInEditorPlugin (status 222) and auto-populate $ROOT in dev mode
|
|
@@ -187,6 +203,11 @@ export function createGgPlugin(options, gg) {
|
|
|
187
203
|
wireUpFilterUI();
|
|
188
204
|
wireUpSettingsUI();
|
|
189
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;
|
|
190
211
|
renderLogs();
|
|
191
212
|
},
|
|
192
213
|
show() {
|
|
@@ -205,6 +226,7 @@ export function createGgPlugin(options, gg) {
|
|
|
205
226
|
gg._onLog = null;
|
|
206
227
|
}
|
|
207
228
|
buffer.clear();
|
|
229
|
+
allNamespacesSet.clear();
|
|
208
230
|
}
|
|
209
231
|
};
|
|
210
232
|
function toggleNamespace(namespace, enable) {
|
|
@@ -286,10 +308,7 @@ export function createGgPlugin(options, gg) {
|
|
|
286
308
|
return parts.join(',');
|
|
287
309
|
}
|
|
288
310
|
function getAllCapturedNamespaces() {
|
|
289
|
-
|
|
290
|
-
const nsSet = new Set();
|
|
291
|
-
entries.forEach((e) => nsSet.add(e.namespace));
|
|
292
|
-
return Array.from(nsSet).sort();
|
|
311
|
+
return Array.from(allNamespacesSet).sort();
|
|
293
312
|
}
|
|
294
313
|
function namespaceMatchesPattern(namespace, pattern) {
|
|
295
314
|
if (!pattern)
|
|
@@ -970,6 +989,7 @@ export function createGgPlugin(options, gg) {
|
|
|
970
989
|
</div>
|
|
971
990
|
<div class="gg-filter-panel"></div>
|
|
972
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>
|
|
973
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>
|
|
974
994
|
<div class="gg-toast"></div>
|
|
975
995
|
<iframe class="gg-editor-iframe" hidden title="open-in-editor"></iframe>
|
|
@@ -1372,6 +1392,7 @@ export function createGgPlugin(options, gg) {
|
|
|
1372
1392
|
return;
|
|
1373
1393
|
$el.find('.gg-clear-btn').on('click', () => {
|
|
1374
1394
|
buffer.clear();
|
|
1395
|
+
allNamespacesSet.clear();
|
|
1375
1396
|
renderLogs();
|
|
1376
1397
|
});
|
|
1377
1398
|
$el.find('.gg-copy-btn').on('click', async () => {
|
|
@@ -1971,21 +1992,238 @@ export function createGgPlugin(options, gg) {
|
|
|
1971
1992
|
containerEl.addEventListener('pointerup', onPointerUp);
|
|
1972
1993
|
resizeAttached = true;
|
|
1973
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.) */
|
|
1974
2215
|
function renderLogs() {
|
|
1975
2216
|
if (!$el)
|
|
1976
2217
|
return;
|
|
1977
2218
|
const logContainer = $el.find('.gg-log-container');
|
|
1978
|
-
|
|
1979
|
-
if (!logContainer.length || !copyCountSpan.length)
|
|
2219
|
+
if (!logContainer.length)
|
|
1980
2220
|
return;
|
|
1981
2221
|
const allEntries = buffer.getEntries();
|
|
1982
2222
|
// Apply filtering
|
|
1983
2223
|
const entries = allEntries.filter((entry) => enabledNamespaces.has(entry.namespace));
|
|
1984
2224
|
renderedEntries = entries;
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
: `Copy ${entries.length} / ${allEntries.length} ${entries.length === 1 ? 'entry' : 'entries'}`;
|
|
1988
|
-
copyCountSpan.html(countText);
|
|
2225
|
+
updateCopyCount();
|
|
2226
|
+
updateTruncationBanner();
|
|
1989
2227
|
if (entries.length === 0) {
|
|
1990
2228
|
const hasFilteredLogs = allEntries.length > 0;
|
|
1991
2229
|
const message = hasFilteredLogs
|
|
@@ -1998,139 +2236,7 @@ export function createGgPlugin(options, gg) {
|
|
|
1998
2236
|
return;
|
|
1999
2237
|
}
|
|
2000
2238
|
const logsHTML = `<div class="gg-log-grid${filterExpanded ? ' filter-mode' : ''}${showExpressions ? ' gg-show-expr' : ''}" style="grid-template-columns: ${gridColumns()};">${entries
|
|
2001
|
-
.map((entry, index) =>
|
|
2002
|
-
const color = entry.color || '#0066cc';
|
|
2003
|
-
const diff = `+${humanize(entry.diff)}`;
|
|
2004
|
-
// Split namespace into clickable segments on multiple delimiters: : @ / - _
|
|
2005
|
-
const parts = entry.namespace.split(/([:/@ \-_])/);
|
|
2006
|
-
const nsSegments = [];
|
|
2007
|
-
const delimiters = [];
|
|
2008
|
-
for (let i = 0; i < parts.length; i++) {
|
|
2009
|
-
if (i % 2 === 0) {
|
|
2010
|
-
// Even indices are segments
|
|
2011
|
-
if (parts[i])
|
|
2012
|
-
nsSegments.push(parts[i]);
|
|
2013
|
-
}
|
|
2014
|
-
else {
|
|
2015
|
-
// Odd indices are delimiters
|
|
2016
|
-
delimiters.push(parts[i]);
|
|
2017
|
-
}
|
|
2018
|
-
}
|
|
2019
|
-
let nsHTML = '';
|
|
2020
|
-
for (let i = 0; i < nsSegments.length; i++) {
|
|
2021
|
-
const segment = escapeHtml(nsSegments[i]);
|
|
2022
|
-
// Build filter pattern: reconstruct namespace up to this point
|
|
2023
|
-
let filterPattern = '';
|
|
2024
|
-
for (let j = 0; j <= i; j++) {
|
|
2025
|
-
filterPattern += nsSegments[j];
|
|
2026
|
-
if (j < i) {
|
|
2027
|
-
filterPattern += delimiters[j];
|
|
2028
|
-
}
|
|
2029
|
-
else if (j < nsSegments.length - 1) {
|
|
2030
|
-
filterPattern += delimiters[j] + '*';
|
|
2031
|
-
}
|
|
2032
|
-
}
|
|
2033
|
-
nsHTML += `<span class="gg-ns-segment" data-filter="${escapeHtml(filterPattern)}">${segment}</span>`;
|
|
2034
|
-
if (i < nsSegments.length - 1) {
|
|
2035
|
-
nsHTML += escapeHtml(delimiters[i]);
|
|
2036
|
-
}
|
|
2037
|
-
}
|
|
2038
|
-
// Format each arg individually - objects are expandable
|
|
2039
|
-
let argsHTML = '';
|
|
2040
|
-
let detailsHTML = '';
|
|
2041
|
-
// Source expression for this entry (used in hover tooltips and expanded details)
|
|
2042
|
-
const srcExpr = entry.src?.trim() && !/^['"`]/.test(entry.src) ? escapeHtml(entry.src) : '';
|
|
2043
|
-
// HTML table rendering for gg.table() entries
|
|
2044
|
-
if (entry.tableData && entry.tableData.keys.length > 0) {
|
|
2045
|
-
const { keys, rows: tableRows } = entry.tableData;
|
|
2046
|
-
const headerCells = keys
|
|
2047
|
-
.map((k) => `<th style="padding: 2px 8px; border: 1px solid #ccc; background: #f0f0f0; font-size: 11px; white-space: nowrap;">${escapeHtml(k)}</th>`)
|
|
2048
|
-
.join('');
|
|
2049
|
-
const bodyRowsHtml = tableRows
|
|
2050
|
-
.map((row) => {
|
|
2051
|
-
const cells = keys
|
|
2052
|
-
.map((k) => {
|
|
2053
|
-
const val = row[k];
|
|
2054
|
-
const display = val === undefined ? '' : escapeHtml(String(val));
|
|
2055
|
-
return `<td style="padding: 2px 8px; border: 1px solid #ddd; font-size: 11px; white-space: nowrap;">${display}</td>`;
|
|
2056
|
-
})
|
|
2057
|
-
.join('');
|
|
2058
|
-
return `<tr>${cells}</tr>`;
|
|
2059
|
-
})
|
|
2060
|
-
.join('');
|
|
2061
|
-
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>`;
|
|
2062
|
-
}
|
|
2063
|
-
else if (entry.args.length > 0) {
|
|
2064
|
-
argsHTML = entry.args
|
|
2065
|
-
.map((arg, argIdx) => {
|
|
2066
|
-
if (typeof arg === 'object' && arg !== null) {
|
|
2067
|
-
// Show expandable object
|
|
2068
|
-
const preview = Array.isArray(arg) ? `Array(${arg.length})` : 'Object';
|
|
2069
|
-
const jsonStr = escapeHtml(JSON.stringify(arg, null, 2));
|
|
2070
|
-
const uniqueId = `${index}-${argIdx}`;
|
|
2071
|
-
// Expression header inside expanded details
|
|
2072
|
-
const srcHeader = srcExpr ? `<div class="gg-details-src">${srcExpr}</div>` : '';
|
|
2073
|
-
// Store details separately to render after the row
|
|
2074
|
-
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>`;
|
|
2075
|
-
// data-entry/data-arg for hover tooltip lookup, data-src for expression context
|
|
2076
|
-
const srcAttr = srcExpr ? ` data-src="${srcExpr}"` : '';
|
|
2077
|
-
const srcIcon = srcExpr ? `<span class="gg-src-icon">\uD83D\uDD0D</span>` : '';
|
|
2078
|
-
// Show expression inline after preview when toggle is enabled
|
|
2079
|
-
const inlineExpr = showExpressions && srcExpr
|
|
2080
|
-
? ` <span class="gg-inline-expr">\u2039${srcExpr}\u203A</span>`
|
|
2081
|
-
: '';
|
|
2082
|
-
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>`;
|
|
2083
|
-
}
|
|
2084
|
-
else {
|
|
2085
|
-
// Parse ANSI codes first, then convert URLs to clickable links
|
|
2086
|
-
const argStr = String(arg);
|
|
2087
|
-
const parsedAnsi = parseAnsiToHtml(argStr);
|
|
2088
|
-
// Note: URL linking happens after ANSI parsing, so links work inside colored text
|
|
2089
|
-
// This is a simple approach - URLs inside ANSI codes won't be linkified
|
|
2090
|
-
// For more complex parsing, we'd need to track ANSI state while matching URLs
|
|
2091
|
-
return `<span>${parsedAnsi}</span>`;
|
|
2092
|
-
}
|
|
2093
|
-
})
|
|
2094
|
-
.join(' ');
|
|
2095
|
-
}
|
|
2096
|
-
// Time diff will be clickable for open-in-editor when file metadata exists
|
|
2097
|
-
// Open-in-editor data attributes (file, line, col)
|
|
2098
|
-
const fileAttr = entry.file ? ` data-file="${escapeHtml(entry.file)}"` : '';
|
|
2099
|
-
const lineAttr = entry.line ? ` data-line="${entry.line}"` : '';
|
|
2100
|
-
const colAttr = entry.col ? ` data-col="${entry.col}"` : '';
|
|
2101
|
-
// Level class for info/warn/error styling
|
|
2102
|
-
const levelClass = entry.level === 'info'
|
|
2103
|
-
? ' gg-level-info'
|
|
2104
|
-
: entry.level === 'warn'
|
|
2105
|
-
? ' gg-level-warn'
|
|
2106
|
-
: entry.level === 'error'
|
|
2107
|
-
? ' gg-level-error'
|
|
2108
|
-
: '';
|
|
2109
|
-
// Stack trace toggle (for error/trace entries with captured stacks)
|
|
2110
|
-
let stackHTML = '';
|
|
2111
|
-
if (entry.stack) {
|
|
2112
|
-
const stackId = `stack-${index}`;
|
|
2113
|
-
stackHTML =
|
|
2114
|
-
`<span class="gg-stack-toggle" data-stack-id="${stackId}">▶ stack</span>` +
|
|
2115
|
-
`<div class="gg-stack-content" data-stack-id="${stackId}">${escapeHtml(entry.stack)}</div>`;
|
|
2116
|
-
}
|
|
2117
|
-
// Desktop: grid layout, Mobile: stacked layout
|
|
2118
|
-
// Expression tooltip: skip table entries (tableData) -- expression is just gg.table(...) which isn't useful
|
|
2119
|
-
const hasSrcExpr = !entry.level && !entry.tableData && entry.src?.trim() && !/^['"`]/.test(entry.src);
|
|
2120
|
-
// For primitives-only entries, append inline expression when showExpressions is enabled
|
|
2121
|
-
const inlineExprForPrimitives = showExpressions && hasSrcExpr && !argsHTML.includes('gg-expand')
|
|
2122
|
-
? ` <span class="gg-inline-expr">\u2039${escapeHtml(entry.src)}\u203A</span>`
|
|
2123
|
-
: '';
|
|
2124
|
-
return (`<div class="gg-log-entry${levelClass}" data-entry="${index}">` +
|
|
2125
|
-
`<div class="gg-log-header">` +
|
|
2126
|
-
`<div class="gg-log-diff" style="color: ${color};"${fileAttr}${lineAttr}${colAttr}>${diff}</div>` +
|
|
2127
|
-
`<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>` +
|
|
2128
|
-
`<div class="gg-log-handle"></div>` +
|
|
2129
|
-
`</div>` +
|
|
2130
|
-
`<div class="gg-log-content"${hasSrcExpr ? ` data-src="${escapeHtml(entry.src)}"` : ''}>${argsHTML}${inlineExprForPrimitives}${stackHTML}</div>` +
|
|
2131
|
-
detailsHTML +
|
|
2132
|
-
`</div>`);
|
|
2133
|
-
})
|
|
2239
|
+
.map((entry, index) => renderEntryHTML(entry, index))
|
|
2134
2240
|
.join('')}</div>`;
|
|
2135
2241
|
logContainer.html(logsHTML);
|
|
2136
2242
|
// Append hover tooltip div (destroyed by .html() each render, so re-create)
|
|
@@ -2158,12 +2264,15 @@ export function createGgPlugin(options, gg) {
|
|
|
2158
2264
|
return Math.round(ms / 60000) + 'm ';
|
|
2159
2265
|
if (abs >= 1000)
|
|
2160
2266
|
return Math.round(ms / 1000) + 's ';
|
|
2161
|
-
return ms + 'ms';
|
|
2267
|
+
return Math.round(ms) + 'ms';
|
|
2162
2268
|
}
|
|
2163
2269
|
function escapeHtml(text) {
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2270
|
+
return text
|
|
2271
|
+
.replace(/&/g, '&')
|
|
2272
|
+
.replace(/</g, '<')
|
|
2273
|
+
.replace(/>/g, '>')
|
|
2274
|
+
.replace(/"/g, '"')
|
|
2275
|
+
.replace(/'/g, ''');
|
|
2167
2276
|
}
|
|
2168
2277
|
/**
|
|
2169
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(),
|