@leftium/gg 0.0.21 → 0.0.23
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/eruda/buffer.d.ts +26 -0
- package/dist/eruda/buffer.js +40 -0
- package/dist/eruda/index.d.ts +24 -0
- package/dist/eruda/index.js +82 -0
- package/dist/eruda/loader.d.ts +9 -0
- package/dist/eruda/loader.js +94 -0
- package/dist/eruda/plugin.d.ts +14 -0
- package/dist/eruda/plugin.js +625 -0
- package/dist/eruda/types.d.ts +52 -0
- package/dist/eruda/types.js +1 -0
- package/dist/gg.d.ts +15 -0
- package/dist/gg.js +100 -3
- package/package.json +21 -14
- package/dist/debug/ms/index.d.ts +0 -2
- package/dist/debug/ms/index.js +0 -162
- package/dist/debug/ms/package.json +0 -38
- package/dist/debug/package.json +0 -64
- package/dist/debug/src/browser.d.ts +0 -90
- package/dist/debug/src/browser.js +0 -272
- package/dist/debug/src/common.d.ts +0 -28
- package/dist/debug/src/common.js +0 -292
- package/dist/debug/src/index.d.ts +0 -101
- package/dist/debug/src/index.js +0 -10
- package/dist/debug/src/node.d.ts +0 -95
- package/dist/debug/src/node.js +0 -263
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
import { LogBuffer } from './buffer.js';
|
|
2
|
+
/**
|
|
3
|
+
* Creates the gg Eruda plugin
|
|
4
|
+
*
|
|
5
|
+
* Uses Eruda's plugin API where $el is a jQuery-like (licia) wrapper.
|
|
6
|
+
* Methods: $el.html(), $el.show(), $el.hide(), $el.find(), $el.on()
|
|
7
|
+
*/
|
|
8
|
+
export function createGgPlugin(options, gg) {
|
|
9
|
+
const buffer = new LogBuffer(options.maxEntries ?? 2000);
|
|
10
|
+
// The licia jQuery-like wrapper Eruda passes to init()
|
|
11
|
+
let $el = null;
|
|
12
|
+
let expanderAttached = false;
|
|
13
|
+
let resizeAttached = false;
|
|
14
|
+
// null = auto (fit content), number = user-dragged px width
|
|
15
|
+
let nsColWidth = null;
|
|
16
|
+
// Filter UI state
|
|
17
|
+
let filterExpanded = false;
|
|
18
|
+
let filterPattern = '';
|
|
19
|
+
let enabledNamespaces = new Set();
|
|
20
|
+
const plugin = {
|
|
21
|
+
name: 'GG',
|
|
22
|
+
init($container) {
|
|
23
|
+
$el = $container;
|
|
24
|
+
// Register the capture hook on gg
|
|
25
|
+
if (gg) {
|
|
26
|
+
gg._onLog = (entry) => {
|
|
27
|
+
buffer.push(entry);
|
|
28
|
+
// Add new namespace to enabledNamespaces if it matches the current pattern
|
|
29
|
+
const effectivePattern = filterPattern || 'gg:*';
|
|
30
|
+
if (namespaceMatchesPattern(entry.namespace, effectivePattern)) {
|
|
31
|
+
enabledNamespaces.add(entry.namespace);
|
|
32
|
+
}
|
|
33
|
+
// Update filter UI if expanded (new namespace may have appeared)
|
|
34
|
+
if (filterExpanded) {
|
|
35
|
+
renderFilterUI();
|
|
36
|
+
}
|
|
37
|
+
renderLogs();
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// Load initial filter state
|
|
41
|
+
filterPattern = localStorage.getItem('debug') || '';
|
|
42
|
+
// Render initial UI
|
|
43
|
+
$el.html(buildHTML());
|
|
44
|
+
wireUpButtons();
|
|
45
|
+
wireUpExpanders();
|
|
46
|
+
wireUpResize();
|
|
47
|
+
wireUpFilterUI();
|
|
48
|
+
renderLogs();
|
|
49
|
+
},
|
|
50
|
+
show() {
|
|
51
|
+
$el.show();
|
|
52
|
+
renderLogs();
|
|
53
|
+
},
|
|
54
|
+
hide() {
|
|
55
|
+
$el.hide();
|
|
56
|
+
},
|
|
57
|
+
destroy() {
|
|
58
|
+
if (gg) {
|
|
59
|
+
gg._onLog = null;
|
|
60
|
+
}
|
|
61
|
+
buffer.clear();
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
function loadFilterState() {
|
|
65
|
+
filterPattern = localStorage.getItem('debug') || '';
|
|
66
|
+
// Rebuild enabledNamespaces based on current pattern and captured logs
|
|
67
|
+
const allNamespaces = getAllCapturedNamespaces();
|
|
68
|
+
enabledNamespaces.clear();
|
|
69
|
+
// If no pattern, default to 'gg:*' (show all gg logs)
|
|
70
|
+
const effectivePattern = filterPattern || 'gg:*';
|
|
71
|
+
allNamespaces.forEach((ns) => {
|
|
72
|
+
if (namespaceMatchesPattern(ns, effectivePattern)) {
|
|
73
|
+
enabledNamespaces.add(ns);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function toggleNamespace(namespace, enable) {
|
|
78
|
+
const currentPattern = filterPattern || 'gg:*';
|
|
79
|
+
const ns = namespace.trim();
|
|
80
|
+
// Split into parts, manipulate, rejoin (avoids fragile regex on complex namespace strings)
|
|
81
|
+
const parts = currentPattern
|
|
82
|
+
.split(',')
|
|
83
|
+
.map((p) => p.trim())
|
|
84
|
+
.filter(Boolean);
|
|
85
|
+
if (enable) {
|
|
86
|
+
// Remove any exclusion for this namespace
|
|
87
|
+
const filtered = parts.filter((p) => p !== `-${ns}`);
|
|
88
|
+
filterPattern = filtered.join(',');
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
// Add exclusion
|
|
92
|
+
parts.push(`-${ns}`);
|
|
93
|
+
filterPattern = parts.join(',');
|
|
94
|
+
}
|
|
95
|
+
// Simplify pattern
|
|
96
|
+
filterPattern = simplifyPattern(filterPattern);
|
|
97
|
+
// Sync enabledNamespaces from the NEW pattern (don't re-read localStorage)
|
|
98
|
+
const allNamespaces = getAllCapturedNamespaces();
|
|
99
|
+
enabledNamespaces.clear();
|
|
100
|
+
const effectivePattern = filterPattern || 'gg:*';
|
|
101
|
+
allNamespaces.forEach((ns) => {
|
|
102
|
+
if (namespaceMatchesPattern(ns, effectivePattern)) {
|
|
103
|
+
enabledNamespaces.add(ns);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function simplifyPattern(pattern) {
|
|
108
|
+
if (!pattern)
|
|
109
|
+
return '';
|
|
110
|
+
// Remove empty parts
|
|
111
|
+
let parts = pattern
|
|
112
|
+
.split(',')
|
|
113
|
+
.map((p) => p.trim())
|
|
114
|
+
.filter(Boolean);
|
|
115
|
+
// Remove duplicates
|
|
116
|
+
parts = Array.from(new Set(parts));
|
|
117
|
+
// Clean up trailing/leading commas
|
|
118
|
+
return parts.join(',');
|
|
119
|
+
}
|
|
120
|
+
function getAllCapturedNamespaces() {
|
|
121
|
+
const entries = buffer.getEntries();
|
|
122
|
+
const nsSet = new Set();
|
|
123
|
+
entries.forEach((e) => nsSet.add(e.namespace));
|
|
124
|
+
return Array.from(nsSet).sort();
|
|
125
|
+
}
|
|
126
|
+
function namespaceMatchesPattern(namespace, pattern) {
|
|
127
|
+
if (!pattern)
|
|
128
|
+
return true; // Empty pattern = show all
|
|
129
|
+
// Split by comma for OR logic
|
|
130
|
+
const parts = pattern.split(',').map((p) => p.trim());
|
|
131
|
+
let included = false;
|
|
132
|
+
let excluded = false;
|
|
133
|
+
for (const part of parts) {
|
|
134
|
+
if (part.startsWith('-')) {
|
|
135
|
+
// Exclusion pattern
|
|
136
|
+
const excludePattern = part.slice(1);
|
|
137
|
+
if (matchesGlob(namespace, excludePattern)) {
|
|
138
|
+
excluded = true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
// Inclusion pattern
|
|
143
|
+
if (matchesGlob(namespace, part)) {
|
|
144
|
+
included = true;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// If no inclusion patterns, default to included
|
|
149
|
+
const hasInclusions = parts.some((p) => !p.startsWith('-'));
|
|
150
|
+
if (!hasInclusions)
|
|
151
|
+
included = true;
|
|
152
|
+
return included && !excluded;
|
|
153
|
+
}
|
|
154
|
+
function matchesGlob(str, pattern) {
|
|
155
|
+
// Trim both for comparison (namespaces may have trailing spaces from padEnd)
|
|
156
|
+
const s = str.trim();
|
|
157
|
+
const p = pattern.trim();
|
|
158
|
+
// Convert glob pattern to regex
|
|
159
|
+
const regexPattern = p
|
|
160
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape special chars
|
|
161
|
+
.replace(/\*/g, '.*'); // * becomes .*
|
|
162
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
163
|
+
return regex.test(s);
|
|
164
|
+
}
|
|
165
|
+
function isSimplePattern(pattern) {
|
|
166
|
+
if (!pattern)
|
|
167
|
+
return true;
|
|
168
|
+
// Simple patterns:
|
|
169
|
+
// 1. 'gg:*' with optional exclusions
|
|
170
|
+
// 2. Explicit comma-separated list of exact namespaces
|
|
171
|
+
const parts = pattern.split(',').map((p) => p.trim());
|
|
172
|
+
// Check if it's 'gg:*' based (with exclusions)
|
|
173
|
+
const hasWildcardBase = parts.some((p) => p === 'gg:*' || p === '*');
|
|
174
|
+
if (hasWildcardBase) {
|
|
175
|
+
// All other parts must be exclusions starting with '-gg:'
|
|
176
|
+
const otherParts = parts.filter((p) => p !== 'gg:*' && p !== '*');
|
|
177
|
+
return otherParts.every((p) => p.startsWith('-') && !p.includes('*', 1));
|
|
178
|
+
}
|
|
179
|
+
// Check if it's an explicit list (no wildcards)
|
|
180
|
+
return parts.every((p) => !p.includes('*') && !p.startsWith('-'));
|
|
181
|
+
}
|
|
182
|
+
function gridColumns() {
|
|
183
|
+
const ns = nsColWidth !== null ? `${nsColWidth}px` : 'auto';
|
|
184
|
+
if (filterExpanded) {
|
|
185
|
+
// [×] | diff | ns | handle | content
|
|
186
|
+
return `24px auto ${ns} 4px 1fr`;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
// diff | ns | handle | content
|
|
190
|
+
return `auto ${ns} 4px 1fr`;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function buildHTML() {
|
|
194
|
+
return `
|
|
195
|
+
<style>
|
|
196
|
+
.gg-log-grid {
|
|
197
|
+
display: grid;
|
|
198
|
+
grid-template-columns: ${gridColumns()};
|
|
199
|
+
column-gap: 0;
|
|
200
|
+
align-items: start !important;
|
|
201
|
+
}
|
|
202
|
+
.gg-log-grid > * {
|
|
203
|
+
min-width: 0;
|
|
204
|
+
border-top: 1px solid rgba(0,0,0,0.05);
|
|
205
|
+
align-self: start !important;
|
|
206
|
+
}
|
|
207
|
+
.gg-details {
|
|
208
|
+
align-self: stretch !important;
|
|
209
|
+
border-bottom: none;
|
|
210
|
+
}
|
|
211
|
+
.gg-log-diff {
|
|
212
|
+
text-align: right;
|
|
213
|
+
padding: 4px 8px 4px 0;
|
|
214
|
+
white-space: pre;
|
|
215
|
+
}
|
|
216
|
+
.gg-log-ns {
|
|
217
|
+
font-weight: bold;
|
|
218
|
+
white-space: nowrap;
|
|
219
|
+
overflow: hidden;
|
|
220
|
+
text-overflow: ellipsis;
|
|
221
|
+
padding: 4px 8px 4px 0;
|
|
222
|
+
}
|
|
223
|
+
.gg-log-handle {
|
|
224
|
+
width: 4px;
|
|
225
|
+
cursor: col-resize;
|
|
226
|
+
align-self: stretch !important;
|
|
227
|
+
background: transparent;
|
|
228
|
+
position: relative;
|
|
229
|
+
padding: 0 8px 0 0;
|
|
230
|
+
}
|
|
231
|
+
/* Wider invisible hit area */
|
|
232
|
+
.gg-log-handle::before {
|
|
233
|
+
content: '';
|
|
234
|
+
position: absolute;
|
|
235
|
+
top: 0; bottom: 0;
|
|
236
|
+
left: -4px; right: -4px;
|
|
237
|
+
}
|
|
238
|
+
.gg-log-handle:hover,
|
|
239
|
+
.gg-log-handle.gg-dragging {
|
|
240
|
+
background: rgba(0,0,0,0.15);
|
|
241
|
+
}
|
|
242
|
+
.gg-log-content {
|
|
243
|
+
word-break: break-word;
|
|
244
|
+
padding: 4px 0;
|
|
245
|
+
}
|
|
246
|
+
.gg-row-filter {
|
|
247
|
+
text-align: center;
|
|
248
|
+
padding: 4px 8px 4px 0;
|
|
249
|
+
cursor: pointer;
|
|
250
|
+
user-select: none;
|
|
251
|
+
opacity: 0.6;
|
|
252
|
+
font-size: 14px;
|
|
253
|
+
align-self: start;
|
|
254
|
+
}
|
|
255
|
+
.gg-row-filter:hover {
|
|
256
|
+
opacity: 1;
|
|
257
|
+
background: rgba(0,0,0,0.05);
|
|
258
|
+
}
|
|
259
|
+
.gg-filter-panel {
|
|
260
|
+
background: #f5f5f5;
|
|
261
|
+
padding: 10px;
|
|
262
|
+
margin-bottom: 8px;
|
|
263
|
+
border-radius: 4px;
|
|
264
|
+
flex-shrink: 0;
|
|
265
|
+
display: none;
|
|
266
|
+
}
|
|
267
|
+
.gg-filter-panel.expanded {
|
|
268
|
+
display: block;
|
|
269
|
+
}
|
|
270
|
+
.gg-filter-pattern {
|
|
271
|
+
width: 100%;
|
|
272
|
+
padding: 4px 8px;
|
|
273
|
+
font-family: monospace;
|
|
274
|
+
font-size: 12px;
|
|
275
|
+
margin-bottom: 8px;
|
|
276
|
+
}
|
|
277
|
+
.gg-filter-checkboxes {
|
|
278
|
+
display: flex;
|
|
279
|
+
flex-wrap: wrap;
|
|
280
|
+
gap: 8px;
|
|
281
|
+
margin: 8px 0;
|
|
282
|
+
max-height: 100px;
|
|
283
|
+
overflow-y: auto;
|
|
284
|
+
}
|
|
285
|
+
.gg-filter-checkbox {
|
|
286
|
+
display: flex;
|
|
287
|
+
align-items: center;
|
|
288
|
+
gap: 4px;
|
|
289
|
+
font-size: 11px;
|
|
290
|
+
font-family: monospace;
|
|
291
|
+
white-space: nowrap;
|
|
292
|
+
}
|
|
293
|
+
</style>
|
|
294
|
+
<div class="eruda-gg" style="padding: 10px; height: 100%; display: flex; flex-direction: column; font-size: 14px;">
|
|
295
|
+
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px; flex-shrink: 0;">
|
|
296
|
+
<button class="gg-clear-btn" style="padding: 4px 10px; cursor: pointer;">Clear</button>
|
|
297
|
+
<button class="gg-copy-btn" style="padding: 4px 10px; cursor: pointer;">Copy</button>
|
|
298
|
+
<button class="gg-filter-btn" style="padding: 4px 10px; cursor: pointer; flex: 1; min-width: 0; text-align: left; font-family: monospace; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">⚙️ Filters: <span class="gg-filter-summary"></span></button>
|
|
299
|
+
<span class="gg-count" style="opacity: 0.6; white-space: nowrap;"></span>
|
|
300
|
+
</div>
|
|
301
|
+
<div class="gg-filter-panel"></div>
|
|
302
|
+
<div class="gg-log-container" style="flex: 1; overflow-y: auto; font-family: monospace; font-size: 12px;"></div>
|
|
303
|
+
</div>
|
|
304
|
+
`;
|
|
305
|
+
}
|
|
306
|
+
function applyPatternFromInput(value) {
|
|
307
|
+
filterPattern = value;
|
|
308
|
+
localStorage.setItem('debug', filterPattern);
|
|
309
|
+
// Sync enabledNamespaces from the new pattern
|
|
310
|
+
const allNamespaces = getAllCapturedNamespaces();
|
|
311
|
+
enabledNamespaces.clear();
|
|
312
|
+
const effectivePattern = filterPattern || 'gg:*';
|
|
313
|
+
allNamespaces.forEach((ns) => {
|
|
314
|
+
if (namespaceMatchesPattern(ns, effectivePattern)) {
|
|
315
|
+
enabledNamespaces.add(ns);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
renderFilterUI();
|
|
319
|
+
renderLogs();
|
|
320
|
+
}
|
|
321
|
+
function wireUpFilterUI() {
|
|
322
|
+
if (!$el)
|
|
323
|
+
return;
|
|
324
|
+
const filterBtn = $el.find('.gg-filter-btn').get(0);
|
|
325
|
+
const filterPanel = $el.find('.gg-filter-panel').get(0);
|
|
326
|
+
if (!filterBtn || !filterPanel)
|
|
327
|
+
return;
|
|
328
|
+
renderFilterUI();
|
|
329
|
+
// Wire up button toggle
|
|
330
|
+
filterBtn.addEventListener('click', () => {
|
|
331
|
+
filterExpanded = !filterExpanded;
|
|
332
|
+
renderFilterUI();
|
|
333
|
+
renderLogs(); // Re-render to update grid columns
|
|
334
|
+
});
|
|
335
|
+
// Wire up pattern input - apply on blur or Enter
|
|
336
|
+
filterPanel.addEventListener('blur', (e) => {
|
|
337
|
+
const target = e.target;
|
|
338
|
+
if (target.classList.contains('gg-filter-pattern')) {
|
|
339
|
+
applyPatternFromInput(target.value);
|
|
340
|
+
}
|
|
341
|
+
}, true); // useCapture for blur (doesn't bubble)
|
|
342
|
+
filterPanel.addEventListener('keydown', (e) => {
|
|
343
|
+
const target = e.target;
|
|
344
|
+
if (target.classList.contains('gg-filter-pattern') && e.key === 'Enter') {
|
|
345
|
+
applyPatternFromInput(target.value);
|
|
346
|
+
target.blur();
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
// Wire up checkboxes
|
|
350
|
+
filterPanel.addEventListener('change', (e) => {
|
|
351
|
+
const target = e.target;
|
|
352
|
+
if (target.classList.contains('gg-ns-checkbox')) {
|
|
353
|
+
const namespace = target.getAttribute('data-namespace');
|
|
354
|
+
if (!namespace)
|
|
355
|
+
return;
|
|
356
|
+
// Toggle namespace in pattern
|
|
357
|
+
toggleNamespace(namespace, target.checked);
|
|
358
|
+
// Re-render to update UI
|
|
359
|
+
renderFilterUI();
|
|
360
|
+
renderLogs();
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
function renderFilterUI() {
|
|
365
|
+
if (!$el)
|
|
366
|
+
return;
|
|
367
|
+
// Update button summary
|
|
368
|
+
const filterSummary = $el.find('.gg-filter-summary').get(0);
|
|
369
|
+
if (filterSummary) {
|
|
370
|
+
const summary = filterPattern || 'gg:*';
|
|
371
|
+
filterSummary.textContent = summary;
|
|
372
|
+
}
|
|
373
|
+
// Update panel
|
|
374
|
+
const filterPanel = $el.find('.gg-filter-panel').get(0);
|
|
375
|
+
if (!filterPanel)
|
|
376
|
+
return;
|
|
377
|
+
if (filterExpanded) {
|
|
378
|
+
// Show panel
|
|
379
|
+
filterPanel.classList.add('expanded');
|
|
380
|
+
// Render expanded view
|
|
381
|
+
const allNamespaces = getAllCapturedNamespaces();
|
|
382
|
+
const simple = isSimplePattern(filterPattern);
|
|
383
|
+
const effectivePattern = filterPattern || 'gg:*';
|
|
384
|
+
let checkboxesHTML = '';
|
|
385
|
+
if (simple && allNamespaces.length > 0) {
|
|
386
|
+
checkboxesHTML = `
|
|
387
|
+
<div class="gg-filter-checkboxes">
|
|
388
|
+
${allNamespaces
|
|
389
|
+
.map((ns) => {
|
|
390
|
+
// Check if namespace matches the current pattern
|
|
391
|
+
const checked = namespaceMatchesPattern(ns, effectivePattern);
|
|
392
|
+
return `
|
|
393
|
+
<label class="gg-filter-checkbox">
|
|
394
|
+
<input type="checkbox" class="gg-ns-checkbox" data-namespace="${escapeHtml(ns)}" ${checked ? 'checked' : ''}>
|
|
395
|
+
<span>${escapeHtml(ns)}</span>
|
|
396
|
+
</label>
|
|
397
|
+
`;
|
|
398
|
+
})
|
|
399
|
+
.join('')}
|
|
400
|
+
</div>
|
|
401
|
+
`;
|
|
402
|
+
}
|
|
403
|
+
else if (!simple) {
|
|
404
|
+
checkboxesHTML = `<div style="opacity: 0.6; font-size: 11px; margin: 8px 0;">⚠️ Complex pattern - edit manually (quick filters disabled)</div>`;
|
|
405
|
+
}
|
|
406
|
+
filterPanel.innerHTML = `
|
|
407
|
+
<div style="margin-bottom: 8px;">
|
|
408
|
+
<input type="text" class="gg-filter-pattern" value="${escapeHtml(filterPattern)}" placeholder="gg:*" style="width: 100%;">
|
|
409
|
+
</div>
|
|
410
|
+
${checkboxesHTML}
|
|
411
|
+
`;
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
// Hide panel
|
|
415
|
+
filterPanel.classList.remove('expanded');
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function wireUpButtons() {
|
|
419
|
+
if (!$el)
|
|
420
|
+
return;
|
|
421
|
+
$el.find('.gg-clear-btn').on('click', () => {
|
|
422
|
+
buffer.clear();
|
|
423
|
+
renderLogs();
|
|
424
|
+
});
|
|
425
|
+
$el.find('.gg-copy-btn').on('click', async () => {
|
|
426
|
+
const entries = buffer.getEntries();
|
|
427
|
+
const text = entries
|
|
428
|
+
.map((e) => {
|
|
429
|
+
const timestamp = new Date(e.timestamp).toISOString();
|
|
430
|
+
// Format args: stringify objects, keep primitives as-is
|
|
431
|
+
const argsStr = e.args
|
|
432
|
+
.map((arg) => {
|
|
433
|
+
if (typeof arg === 'object' && arg !== null) {
|
|
434
|
+
return JSON.stringify(arg, null, 2);
|
|
435
|
+
}
|
|
436
|
+
return String(arg);
|
|
437
|
+
})
|
|
438
|
+
.join(' ');
|
|
439
|
+
return `[${timestamp}] ${e.namespace} ${argsStr}`;
|
|
440
|
+
})
|
|
441
|
+
.join('\n');
|
|
442
|
+
try {
|
|
443
|
+
await navigator.clipboard.writeText(text);
|
|
444
|
+
}
|
|
445
|
+
catch {
|
|
446
|
+
// Fallback: select and copy
|
|
447
|
+
const textarea = document.createElement('textarea');
|
|
448
|
+
textarea.value = text;
|
|
449
|
+
document.body.appendChild(textarea);
|
|
450
|
+
textarea.select();
|
|
451
|
+
document.execCommand('copy');
|
|
452
|
+
document.body.removeChild(textarea);
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
function wireUpExpanders() {
|
|
457
|
+
if (!$el || expanderAttached)
|
|
458
|
+
return;
|
|
459
|
+
// Use native event delegation on the actual DOM element.
|
|
460
|
+
// Licia's .on() doesn't delegate to children replaced by .html().
|
|
461
|
+
const containerEl = $el.find('.gg-log-container').get(0);
|
|
462
|
+
if (!containerEl)
|
|
463
|
+
return;
|
|
464
|
+
containerEl.addEventListener('click', (e) => {
|
|
465
|
+
const target = e.target;
|
|
466
|
+
// Handle expand/collapse
|
|
467
|
+
if (target?.classList?.contains('gg-expand')) {
|
|
468
|
+
const index = target.getAttribute('data-index');
|
|
469
|
+
if (!index)
|
|
470
|
+
return;
|
|
471
|
+
const details = containerEl.querySelector(`.gg-details[data-index="${index}"]`);
|
|
472
|
+
if (details) {
|
|
473
|
+
details.style.display = details.style.display === 'none' ? 'block' : 'none';
|
|
474
|
+
}
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
// Handle row filter button
|
|
478
|
+
if (target?.classList?.contains('gg-row-filter')) {
|
|
479
|
+
const namespace = target.getAttribute('data-namespace');
|
|
480
|
+
if (!namespace)
|
|
481
|
+
return;
|
|
482
|
+
// Toggle this namespace off
|
|
483
|
+
toggleNamespace(namespace, false);
|
|
484
|
+
// Save to localStorage and re-render
|
|
485
|
+
localStorage.setItem('debug', filterPattern);
|
|
486
|
+
renderFilterUI();
|
|
487
|
+
renderLogs();
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
expanderAttached = true;
|
|
491
|
+
}
|
|
492
|
+
function wireUpResize() {
|
|
493
|
+
if (!$el || resizeAttached)
|
|
494
|
+
return;
|
|
495
|
+
const containerEl = $el.find('.gg-log-container').get(0);
|
|
496
|
+
if (!containerEl)
|
|
497
|
+
return;
|
|
498
|
+
let dragging = false;
|
|
499
|
+
let startX = 0;
|
|
500
|
+
let startWidth = 0;
|
|
501
|
+
function onPointerDown(e) {
|
|
502
|
+
const target = e.target;
|
|
503
|
+
if (!target?.classList?.contains('gg-log-handle'))
|
|
504
|
+
return;
|
|
505
|
+
e.preventDefault();
|
|
506
|
+
dragging = true;
|
|
507
|
+
target.classList.add('gg-dragging');
|
|
508
|
+
target.setPointerCapture(e.pointerId);
|
|
509
|
+
startX = e.clientX;
|
|
510
|
+
// Measure current namespace column width from a sibling .gg-log-ns
|
|
511
|
+
const grid = containerEl.querySelector('.gg-log-grid');
|
|
512
|
+
const nsCell = grid?.querySelector('.gg-log-ns');
|
|
513
|
+
startWidth = nsCell ? nsCell.getBoundingClientRect().width : 200;
|
|
514
|
+
}
|
|
515
|
+
function onPointerMove(e) {
|
|
516
|
+
if (!dragging)
|
|
517
|
+
return;
|
|
518
|
+
const delta = e.clientX - startX;
|
|
519
|
+
const newWidth = Math.max(40, startWidth + delta);
|
|
520
|
+
nsColWidth = newWidth;
|
|
521
|
+
// Update grid template on the live element (no full re-render)
|
|
522
|
+
const grid = containerEl.querySelector('.gg-log-grid');
|
|
523
|
+
if (grid) {
|
|
524
|
+
grid.style.gridTemplateColumns = gridColumns();
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
function onPointerUp(e) {
|
|
528
|
+
if (!dragging)
|
|
529
|
+
return;
|
|
530
|
+
dragging = false;
|
|
531
|
+
const target = e.target;
|
|
532
|
+
target?.classList?.remove('gg-dragging');
|
|
533
|
+
}
|
|
534
|
+
containerEl.addEventListener('pointerdown', onPointerDown);
|
|
535
|
+
containerEl.addEventListener('pointermove', onPointerMove);
|
|
536
|
+
containerEl.addEventListener('pointerup', onPointerUp);
|
|
537
|
+
resizeAttached = true;
|
|
538
|
+
}
|
|
539
|
+
function renderLogs() {
|
|
540
|
+
if (!$el)
|
|
541
|
+
return;
|
|
542
|
+
const logContainer = $el.find('.gg-log-container');
|
|
543
|
+
const countSpan = $el.find('.gg-count');
|
|
544
|
+
if (!logContainer.length || !countSpan.length)
|
|
545
|
+
return;
|
|
546
|
+
const allEntries = buffer.getEntries();
|
|
547
|
+
// Apply filtering
|
|
548
|
+
const entries = allEntries.filter((entry) => enabledNamespaces.has(entry.namespace));
|
|
549
|
+
const countText = entries.length === allEntries.length
|
|
550
|
+
? `${entries.length} entries`
|
|
551
|
+
: `${entries.length} / ${allEntries.length} entries`;
|
|
552
|
+
countSpan.html(countText);
|
|
553
|
+
if (entries.length === 0) {
|
|
554
|
+
logContainer.html('<div style="padding: 20px; text-align: center; opacity: 0.5;">No logs captured yet. Call gg() to see output here.</div>');
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
const logsHTML = `<div class="gg-log-grid" style="grid-template-columns: ${gridColumns()};">${entries
|
|
558
|
+
.map((entry, index) => {
|
|
559
|
+
const color = entry.color || '#0066cc';
|
|
560
|
+
const diff = `+${humanize(entry.diff)}`;
|
|
561
|
+
const ns = escapeHtml(entry.namespace);
|
|
562
|
+
// Format each arg individually - objects are expandable
|
|
563
|
+
let argsHTML = '';
|
|
564
|
+
let detailsHTML = '';
|
|
565
|
+
if (entry.args.length === 0) {
|
|
566
|
+
argsHTML = '';
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
argsHTML = entry.args
|
|
570
|
+
.map((arg, argIdx) => {
|
|
571
|
+
if (typeof arg === 'object' && arg !== null) {
|
|
572
|
+
// Show expandable object
|
|
573
|
+
const preview = Array.isArray(arg) ? `Array(${arg.length})` : 'Object';
|
|
574
|
+
const jsonStr = escapeHtml(JSON.stringify(arg, null, 2));
|
|
575
|
+
const uniqueId = `${index}-${argIdx}`;
|
|
576
|
+
// Store details separately to render after the row
|
|
577
|
+
detailsHTML += `<div class="gg-details" data-index="${uniqueId}" style="display: none; grid-column: 1 / -1; margin: 4px 0 8px 0; padding: 8px; background: #f8f8f8; border-left: 3px solid ${color}; font-size: 11px; overflow-x: auto;"><pre style="margin: 0;">${jsonStr}</pre></div>`;
|
|
578
|
+
return `<span style="color: #888; cursor: pointer; text-decoration: underline;" class="gg-expand" data-index="${uniqueId}">${preview}</span>`;
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
return `<span>${escapeHtml(String(arg))}</span>`;
|
|
582
|
+
}
|
|
583
|
+
})
|
|
584
|
+
.join(' ');
|
|
585
|
+
}
|
|
586
|
+
// Add filter button if expanded
|
|
587
|
+
const filterBtn = filterExpanded
|
|
588
|
+
? `<div class="gg-row-filter" data-namespace="${ns}" title="Hide this namespace">×</div>`
|
|
589
|
+
: '';
|
|
590
|
+
return (filterBtn +
|
|
591
|
+
`<div class="gg-log-diff" style="color: ${color};">${diff}</div>` +
|
|
592
|
+
`<div class="gg-log-ns" style="color: ${color};">${ns}</div>` +
|
|
593
|
+
`<div class="gg-log-handle"></div>` +
|
|
594
|
+
`<div class="gg-log-content">${argsHTML}</div>` +
|
|
595
|
+
detailsHTML);
|
|
596
|
+
})
|
|
597
|
+
.join('')}</div>`;
|
|
598
|
+
logContainer.html(logsHTML);
|
|
599
|
+
// Re-wire expanders after rendering
|
|
600
|
+
wireUpExpanders();
|
|
601
|
+
// Auto-scroll to bottom
|
|
602
|
+
const el = logContainer.get(0);
|
|
603
|
+
if (el)
|
|
604
|
+
el.scrollTop = el.scrollHeight;
|
|
605
|
+
}
|
|
606
|
+
/** Format ms like debug's `ms` package: 0ms, 500ms, 5s, 2m, 1h, 3d */
|
|
607
|
+
function humanize(ms) {
|
|
608
|
+
const abs = Math.abs(ms);
|
|
609
|
+
if (abs >= 86400000)
|
|
610
|
+
return Math.round(ms / 86400000) + 'd ';
|
|
611
|
+
if (abs >= 3600000)
|
|
612
|
+
return Math.round(ms / 3600000) + 'h ';
|
|
613
|
+
if (abs >= 60000)
|
|
614
|
+
return Math.round(ms / 60000) + 'm ';
|
|
615
|
+
if (abs >= 1000)
|
|
616
|
+
return Math.round(ms / 1000) + 's ';
|
|
617
|
+
return ms + 'ms';
|
|
618
|
+
}
|
|
619
|
+
function escapeHtml(text) {
|
|
620
|
+
const div = document.createElement('div');
|
|
621
|
+
div.textContent = text;
|
|
622
|
+
return div.innerHTML;
|
|
623
|
+
}
|
|
624
|
+
return plugin;
|
|
625
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for initializing the gg Eruda plugin
|
|
3
|
+
*/
|
|
4
|
+
export interface GgErudaOptions {
|
|
5
|
+
/**
|
|
6
|
+
* How to load in production
|
|
7
|
+
* @default ['url-param', 'gesture']
|
|
8
|
+
*/
|
|
9
|
+
prod?: Array<'url-param' | 'localStorage' | 'gesture'> | 'url-param' | 'localStorage' | 'gesture' | false;
|
|
10
|
+
/**
|
|
11
|
+
* Max captured log entries (ring buffer)
|
|
12
|
+
* @default 2000
|
|
13
|
+
*/
|
|
14
|
+
maxEntries?: number;
|
|
15
|
+
/**
|
|
16
|
+
* Auto-enable localStorage.debug = 'gg:*' if unset
|
|
17
|
+
* @default true
|
|
18
|
+
*/
|
|
19
|
+
autoEnable?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Additional Eruda options passed to eruda.init()
|
|
22
|
+
* @default {}
|
|
23
|
+
*/
|
|
24
|
+
erudaOptions?: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* A captured log entry from gg()
|
|
28
|
+
*/
|
|
29
|
+
export interface CapturedEntry {
|
|
30
|
+
/** Namespace (e.g., "gg:routes/+page.svelte@handleClick") */
|
|
31
|
+
namespace: string;
|
|
32
|
+
/** Color assigned by the debug library (e.g., "#CC3366") */
|
|
33
|
+
color: string;
|
|
34
|
+
/** Millisecond diff from previous log (e.g., 0, 123) */
|
|
35
|
+
diff: number;
|
|
36
|
+
/** Formatted message string */
|
|
37
|
+
message: string;
|
|
38
|
+
/** Raw arguments for expandable view */
|
|
39
|
+
args: unknown[];
|
|
40
|
+
/** Timestamp */
|
|
41
|
+
timestamp: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Eruda plugin interface
|
|
45
|
+
*/
|
|
46
|
+
export interface ErudaPlugin {
|
|
47
|
+
name: string;
|
|
48
|
+
init($el: HTMLElement): void;
|
|
49
|
+
show?(): void;
|
|
50
|
+
hide?(): void;
|
|
51
|
+
destroy?(): void;
|
|
52
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|