@leftium/gg 0.0.25 → 0.0.27

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.
@@ -62,6 +62,17 @@ export async function loadEruda(options) {
62
62
  // Dynamic import of Eruda
63
63
  const erudaModule = await import('eruda');
64
64
  const eruda = erudaModule.default;
65
+ // Clear Eruda position state to prevent icon from being stuck in wrong position
66
+ // Eruda stores draggable icon position in localStorage which can get corrupted
67
+ // This ensures the icon always appears in the default bottom-right corner
68
+ try {
69
+ // Eruda uses keys like 'eruda-entry-button' for position state
70
+ const positionKeys = ['eruda-entry-button', 'eruda-position'];
71
+ positionKeys.forEach((key) => localStorage.removeItem(key));
72
+ }
73
+ catch {
74
+ // localStorage might not be available
75
+ }
65
76
  // Initialize Eruda
66
77
  eruda.init({
67
78
  ...options.erudaOptions,
@@ -81,13 +92,15 @@ export async function loadEruda(options) {
81
92
  }
82
93
  // Register gg plugin
83
94
  // Import gg and pass it to the plugin directly
84
- const { gg } = await import('../gg.js');
95
+ const { gg, runGgDiagnostics } = await import('../gg.js');
85
96
  const { createGgPlugin } = await import('./plugin.js');
86
97
  const ggPlugin = createGgPlugin(options, gg);
87
98
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
88
99
  eruda.add(ggPlugin);
89
100
  // Make GG tab the default selected tab
90
101
  eruda.show('GG');
102
+ // Run diagnostics after Eruda is ready so they appear in Console tab
103
+ await runGgDiagnostics();
91
104
  }
92
105
  catch (error) {
93
106
  console.error('[gg] Failed to load Eruda:', error);
@@ -172,29 +172,37 @@ export function createGgPlugin(options, gg) {
172
172
  }
173
173
  function gridColumns() {
174
174
  const ns = nsColWidth !== null ? `${nsColWidth}px` : 'auto';
175
- if (filterExpanded) {
176
- // [×] | diff | ns | handle | content
177
- return `24px auto ${ns} 4px 1fr`;
178
- }
179
- else {
180
- // diff | ns | handle | content
181
- return `auto ${ns} 4px 1fr`;
182
- }
175
+ // diff | ns | handle | content (× is now inside ns)
176
+ return `auto ${ns} 4px 1fr`;
183
177
  }
184
178
  function buildHTML() {
185
179
  return `
186
180
  <style>
187
- .gg-log-grid {
188
- display: grid;
189
- grid-template-columns: ${gridColumns()};
190
- column-gap: 0;
191
- align-items: start !important;
192
- }
193
- .gg-log-grid > * {
194
- min-width: 0;
195
- border-top: 1px solid rgba(0,0,0,0.05);
196
- align-self: start !important;
197
- }
181
+ .gg-log-grid {
182
+ display: grid;
183
+ grid-template-columns: ${gridColumns()};
184
+ column-gap: 0;
185
+ align-items: start !important;
186
+ }
187
+ /* Desktop: hide wrapper divs, show direct children */
188
+ .gg-log-entry {
189
+ display: contents;
190
+ }
191
+ .gg-log-header {
192
+ display: contents;
193
+ }
194
+ .gg-log-diff,
195
+ .gg-log-ns,
196
+ .gg-log-handle,
197
+ .gg-log-content {
198
+ min-width: 0;
199
+ align-self: start !important;
200
+ border-top: 1px solid rgba(0,0,0,0.05);
201
+ }
202
+ .gg-details {
203
+ grid-column: 1 / -1;
204
+ border-top: none;
205
+ }
198
206
  .gg-details {
199
207
  align-self: stretch !important;
200
208
  border-bottom: none;
@@ -234,19 +242,15 @@ export function createGgPlugin(options, gg) {
234
242
  word-break: break-word;
235
243
  padding: 4px 0;
236
244
  }
237
- .gg-row-filter {
238
- text-align: center;
239
- padding: 4px 8px 4px 0;
240
- cursor: pointer;
241
- user-select: none;
242
- opacity: 0.6;
243
- font-size: 14px;
244
- align-self: start;
245
- }
246
- .gg-row-filter:hover {
247
- opacity: 1;
248
- background: rgba(0,0,0,0.05);
249
- }
245
+ /* Make header clickable for filtering when filters are expanded */
246
+ .gg-log-header.clickable {
247
+ cursor: pointer;
248
+ }
249
+ /* Desktop: highlight child elements since header has display: contents */
250
+ .gg-log-header.clickable:hover .gg-log-diff,
251
+ .gg-log-header.clickable:hover .gg-log-ns {
252
+ background: rgba(0,0,0,0.05);
253
+ }
250
254
  .gg-filter-panel {
251
255
  background: #f5f5f5;
252
256
  padding: 10px;
@@ -262,7 +266,7 @@ export function createGgPlugin(options, gg) {
262
266
  width: 100%;
263
267
  padding: 4px 8px;
264
268
  font-family: monospace;
265
- font-size: 12px;
269
+ font-size: 16px;
266
270
  margin-bottom: 8px;
267
271
  }
268
272
  .gg-filter-checkboxes {
@@ -281,16 +285,125 @@ export function createGgPlugin(options, gg) {
281
285
  font-family: monospace;
282
286
  white-space: nowrap;
283
287
  }
288
+ /* Mobile responsive styles */
289
+ .gg-toolbar {
290
+ display: flex;
291
+ align-items: center;
292
+ gap: 8px;
293
+ margin-bottom: 8px;
294
+ flex-shrink: 0;
295
+ overflow-x: auto;
296
+ -webkit-overflow-scrolling: touch;
297
+ }
298
+ .gg-toolbar button {
299
+ padding: 4px 10px;
300
+ cursor: pointer;
301
+ flex-shrink: 0;
302
+ }
303
+ .gg-btn-text {
304
+ display: inline;
305
+ }
306
+ .gg-btn-icon {
307
+ display: none;
308
+ }
309
+ @media (max-width: 640px) {
310
+ .gg-btn-text {
311
+ display: none;
312
+ }
313
+ .gg-btn-icon {
314
+ display: inline;
315
+ }
316
+ .gg-toolbar button {
317
+ padding: 4px 8px;
318
+ min-width: 32px;
319
+ }
320
+ .gg-filter-btn {
321
+ font-family: monospace;
322
+ font-size: 12px;
323
+ }
324
+ /* Stack log entries vertically on mobile */
325
+ .gg-log-grid {
326
+ display: block;
327
+ }
328
+ .gg-log-entry {
329
+ display: block;
330
+ padding: 8px 0;
331
+ }
332
+ /* Remove double borders on mobile - only border on entry wrapper */
333
+ .gg-log-entry:not(:first-child) {
334
+ border-top: 1px solid rgba(0,0,0,0.05);
335
+ }
336
+ .gg-log-diff,
337
+ .gg-log-ns,
338
+ .gg-log-handle,
339
+ .gg-log-content,
340
+ .gg-details {
341
+ border-top: none !important;
342
+ }
343
+ .gg-log-header {
344
+ display: flex;
345
+ align-items: center;
346
+ gap: 8px;
347
+ margin-bottom: 4px;
348
+ min-width: 0;
349
+ }
350
+ /* Mobile: hover on container since it's not display: contents */
351
+ .gg-log-header.clickable {
352
+ padding: 2px 0;
353
+ }
354
+ .gg-log-header.clickable:hover {
355
+ background: rgba(0,0,0,0.05);
356
+ }
357
+ /* Override desktop child hover on mobile */
358
+ .gg-log-header.clickable:hover .gg-log-diff,
359
+ .gg-log-header.clickable:hover .gg-log-ns {
360
+ background: transparent;
361
+ }
362
+ .gg-log-diff {
363
+ padding: 0;
364
+ text-align: left;
365
+ flex-shrink: 0;
366
+ white-space: nowrap;
367
+ }
368
+ .gg-log-ns {
369
+ padding: 0;
370
+ flex: 1;
371
+ min-width: 0;
372
+ overflow: hidden;
373
+ text-overflow: ellipsis;
374
+ white-space: nowrap;
375
+ }
376
+ .gg-log-handle {
377
+ display: none;
378
+ }
379
+ .gg-log-content {
380
+ padding: 0;
381
+ padding-left: 0;
382
+ }
383
+ .gg-details {
384
+ margin-top: 4px;
385
+ }
386
+ }
284
387
  </style>
285
- <div class="eruda-gg" style="padding: 10px; height: 100%; display: flex; flex-direction: column; font-size: 14px;">
286
- <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px; flex-shrink: 0;">
287
- <button class="gg-clear-btn" style="padding: 4px 10px; cursor: pointer;">Clear</button>
288
- <button class="gg-copy-btn" style="padding: 4px 10px; cursor: pointer;">Copy</button>
289
- <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>
290
- <span class="gg-count" style="opacity: 0.6; white-space: nowrap;"></span>
291
- </div>
388
+ <div class="eruda-gg" style="padding: 10px; height: 100%; display: flex; flex-direction: column; font-size: 14px; touch-action: none; overscroll-behavior: contain;">
389
+ <div class="gg-toolbar">
390
+ <button class="gg-copy-btn">
391
+ <span class="gg-btn-text">Copy</span>
392
+ <span class="gg-btn-icon" title="Copy">📋</span>
393
+ </button>
394
+ <button class="gg-filter-btn" style="text-align: left; white-space: nowrap;">
395
+ <span class="gg-btn-text">Namespaces: </span>
396
+ <span class="gg-btn-icon">NS: </span>
397
+ <span class="gg-filter-summary"></span>
398
+ </button>
399
+ <span class="gg-count" style="opacity: 0.6; white-space: nowrap; flex: 1; text-align: right;"></span>
400
+ <button class="gg-clear-btn">
401
+ <span class="gg-btn-text">Clear</span>
402
+ <span class="gg-btn-icon" title="Clear">⊘</span>
403
+ </button>
404
+ </div>
292
405
  <div class="gg-filter-panel"></div>
293
- <div class="gg-log-container" style="flex: 1; overflow-y: auto; font-family: monospace; font-size: 12px;"></div>
406
+ <div class="gg-log-container" style="flex: 1; overflow-y: auto; font-family: monospace; font-size: 12px; touch-action: pan-y; overscroll-behavior: contain;"></div>
294
407
  </div>
295
408
  `;
296
409
  }
@@ -340,6 +453,27 @@ export function createGgPlugin(options, gg) {
340
453
  // Wire up checkboxes
341
454
  filterPanel.addEventListener('change', (e) => {
342
455
  const target = e.target;
456
+ // Handle ALL checkbox
457
+ if (target.classList.contains('gg-all-checkbox')) {
458
+ const allNamespaces = getAllCapturedNamespaces();
459
+ if (target.checked) {
460
+ // Select all
461
+ filterPattern = 'gg:*';
462
+ enabledNamespaces.clear();
463
+ allNamespaces.forEach((ns) => enabledNamespaces.add(ns));
464
+ }
465
+ else {
466
+ // Deselect all
467
+ const exclusions = allNamespaces.map((ns) => `-${ns}`).join(',');
468
+ filterPattern = `gg:*,${exclusions}`;
469
+ enabledNamespaces.clear();
470
+ }
471
+ localStorage.setItem('debug', filterPattern);
472
+ renderFilterUI();
473
+ renderLogs();
474
+ return;
475
+ }
476
+ // Handle individual namespace checkboxes
343
477
  if (target.classList.contains('gg-ns-checkbox')) {
344
478
  const namespace = target.getAttribute('data-namespace');
345
479
  if (!namespace)
@@ -355,11 +489,13 @@ export function createGgPlugin(options, gg) {
355
489
  function renderFilterUI() {
356
490
  if (!$el)
357
491
  return;
358
- // Update button summary
492
+ const allNamespaces = getAllCapturedNamespaces();
493
+ const enabledCount = enabledNamespaces.size;
494
+ const totalCount = allNamespaces.length;
495
+ // Update button summary with count of enabled namespaces
359
496
  const filterSummary = $el.find('.gg-filter-summary').get(0);
360
497
  if (filterSummary) {
361
- const summary = filterPattern || 'gg:*';
362
- filterSummary.textContent = summary;
498
+ filterSummary.textContent = `${enabledCount}/${totalCount}`;
363
499
  }
364
500
  // Update panel
365
501
  const filterPanel = $el.find('.gg-filter-panel').get(0);
@@ -374,32 +510,37 @@ export function createGgPlugin(options, gg) {
374
510
  const effectivePattern = filterPattern || 'gg:*';
375
511
  let checkboxesHTML = '';
376
512
  if (simple && allNamespaces.length > 0) {
513
+ const allChecked = enabledCount === totalCount;
377
514
  checkboxesHTML = `
378
- <div class="gg-filter-checkboxes">
379
- ${allNamespaces
515
+ <div class="gg-filter-checkboxes">
516
+ <label class="gg-filter-checkbox" style="font-weight: bold;">
517
+ <input type="checkbox" class="gg-all-checkbox" ${allChecked ? 'checked' : ''}>
518
+ <span>ALL</span>
519
+ </label>
520
+ ${allNamespaces
380
521
  .map((ns) => {
381
522
  // Check if namespace matches the current pattern
382
523
  const checked = namespaceMatchesPattern(ns, effectivePattern);
383
524
  return `
384
- <label class="gg-filter-checkbox">
385
- <input type="checkbox" class="gg-ns-checkbox" data-namespace="${escapeHtml(ns)}" ${checked ? 'checked' : ''}>
386
- <span>${escapeHtml(ns)}</span>
387
- </label>
388
- `;
525
+ <label class="gg-filter-checkbox">
526
+ <input type="checkbox" class="gg-ns-checkbox" data-namespace="${escapeHtml(ns)}" ${checked ? 'checked' : ''}>
527
+ <span>${escapeHtml(ns)}</span>
528
+ </label>
529
+ `;
389
530
  })
390
531
  .join('')}
391
- </div>
392
- `;
532
+ </div>
533
+ `;
393
534
  }
394
535
  else if (!simple) {
395
536
  checkboxesHTML = `<div style="opacity: 0.6; font-size: 11px; margin: 8px 0;">⚠️ Complex pattern - edit manually (quick filters disabled)</div>`;
396
537
  }
397
538
  filterPanel.innerHTML = `
398
- <div style="margin-bottom: 8px;">
399
- <input type="text" class="gg-filter-pattern" value="${escapeHtml(filterPattern)}" placeholder="gg:*" style="width: 100%;">
400
- </div>
401
- ${checkboxesHTML}
402
- `;
539
+ <div style="margin-bottom: 8px;">
540
+ <input type="text" class="gg-filter-pattern" value="${escapeHtml(filterPattern)}" placeholder="gg:*" style="width: 100%;">
541
+ </div>
542
+ ${checkboxesHTML}
543
+ `;
403
544
  }
404
545
  else {
405
546
  // Hide panel
@@ -470,9 +611,12 @@ export function createGgPlugin(options, gg) {
470
611
  }
471
612
  return;
472
613
  }
473
- // Handle row filter button
474
- if (target?.classList?.contains('gg-row-filter')) {
475
- const namespace = target.getAttribute('data-namespace');
614
+ // Handle clickable header (when filters expanded)
615
+ // Skip if clicking on resize handle
616
+ if (!target?.classList?.contains('gg-log-handle') &&
617
+ target?.closest('.gg-log-header.clickable')) {
618
+ const header = target.closest('.gg-log-header.clickable');
619
+ const namespace = header.getAttribute('data-namespace');
476
620
  if (!namespace)
477
621
  return;
478
622
  // Toggle this namespace off
@@ -567,25 +711,40 @@ export function createGgPlugin(options, gg) {
567
711
  const jsonStr = escapeHtml(JSON.stringify(arg, null, 2));
568
712
  const uniqueId = `${index}-${argIdx}`;
569
713
  // Store details separately to render after the row
570
- 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>`;
714
+ 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;"><pre style="margin: 0;">${jsonStr}</pre></div>`;
571
715
  return `<span style="color: #888; cursor: pointer; text-decoration: underline;" class="gg-expand" data-index="${uniqueId}">${preview}</span>`;
572
716
  }
573
717
  else {
574
- return `<span>${escapeHtml(String(arg))}</span>`;
718
+ // Convert URLs to clickable links
719
+ const argStr = String(arg);
720
+ const urlRegex = /(https?:\/\/[^\s]+)/g;
721
+ const linkedText = argStr.replace(urlRegex, (url) => {
722
+ return `<a href="${escapeHtml(url)}" target="_blank" style="color: #0066cc; text-decoration: underline;">${escapeHtml(url)}</a>`;
723
+ });
724
+ return `<span>${linkedText}</span>`;
575
725
  }
576
726
  })
577
727
  .join(' ');
578
728
  }
579
- // Add filter button if expanded
580
- const filterBtn = filterExpanded
581
- ? `<div class="gg-row-filter" data-namespace="${ns}" title="Hide this namespace">×</div>`
729
+ // Make header clickable when filters expanded
730
+ const headerClass = filterExpanded ? 'gg-log-header clickable' : 'gg-log-header';
731
+ const headerAttrs = filterExpanded
732
+ ? ` data-namespace="${ns}" title="Click to hide this namespace"`
733
+ : '';
734
+ // Add × at start of diff when filters expanded (bold, darker)
735
+ const filterIcon = filterExpanded
736
+ ? '<span style="font-weight: bold; color: #000; opacity: 0.6;">× </span>'
582
737
  : '';
583
- return (filterBtn +
584
- `<div class="gg-log-diff" style="color: ${color};">${diff}</div>` +
738
+ // Desktop: grid layout, Mobile: stacked layout
739
+ return (`<div class="gg-log-entry">` +
740
+ `<div class="${headerClass}"${headerAttrs}>` +
741
+ `<div class="gg-log-diff" style="color: ${color};">${filterIcon}${diff}</div>` +
585
742
  `<div class="gg-log-ns" style="color: ${color};">${ns}</div>` +
586
743
  `<div class="gg-log-handle"></div>` +
744
+ `</div>` +
587
745
  `<div class="gg-log-content">${argsHTML}</div>` +
588
- detailsHTML);
746
+ detailsHTML +
747
+ `</div>`);
589
748
  })
590
749
  .join('')}</div>`;
591
750
  logContainer.html(logsHTML);
package/dist/gg.d.ts CHANGED
@@ -22,6 +22,13 @@ export declare namespace gg {
22
22
  var disable: () => void;
23
23
  var enable: (namespaces: string) => void;
24
24
  var clearPersist: () => void;
25
- var _onLog: OnLogCallback | null;
26
25
  }
26
+ export declare namespace gg {
27
+ let _onLog: OnLogCallback | null;
28
+ }
29
+ /**
30
+ * Run gg diagnostics and log configuration status
31
+ * Can be called immediately or delayed (e.g., after Eruda loads)
32
+ */
33
+ export declare function runGgDiagnostics(): Promise<void>;
27
34
  export {};
package/dist/gg.js CHANGED
@@ -1,6 +1,33 @@
1
1
  import debugFactory from './debug.js';
2
2
  import ErrorStackParser from 'error-stack-parser';
3
3
  import { BROWSER, DEV } from 'esm-env';
4
+ /**
5
+ * Creates a debug instance with custom formatArgs to add namespace padding
6
+ * Padding is done at format time, not in the namespace itself, to keep colors stable
7
+ */
8
+ function createGgDebugger(namespace) {
9
+ const dbg = debugFactory(namespace);
10
+ // Store the original formatArgs (if it exists)
11
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
+ const originalFormatArgs = dbg.formatArgs;
13
+ // Override formatArgs to add padding to the namespace display
14
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
+ dbg.formatArgs = function (args) {
16
+ // Call original formatArgs first
17
+ if (originalFormatArgs) {
18
+ originalFormatArgs.call(this, args);
19
+ }
20
+ // Extract the callpoint from namespace (strip 'gg:' prefix and any URL suffix)
21
+ const nsMatch = this.namespace.match(/^gg:([^h]+?)(?:http|$)/);
22
+ const callpoint = nsMatch ? nsMatch[1] : this.namespace.replace(/^gg:/, '');
23
+ const paddedCallpoint = callpoint.padEnd(maxCallpointLength, ' ');
24
+ // Replace the namespace in the formatted string with padded version
25
+ if (typeof args[0] === 'string') {
26
+ args[0] = args[0].replace(this.namespace, `gg:${paddedCallpoint}`);
27
+ }
28
+ };
29
+ return dbg;
30
+ }
4
31
  // Helper to detect if we're running in CloudFlare Workers
5
32
  const isCloudflareWorker = () => {
6
33
  // Check for CloudFlare Workers-specific global
@@ -176,40 +203,57 @@ export function gg(...args) {
176
203
  if (callpoint.length < 80 && callpoint.length > maxCallpointLength) {
177
204
  maxCallpointLength = callpoint.length;
178
205
  }
179
- namespace = `gg:${callpoint.padEnd(maxCallpointLength, ' ')}${ggConfig.editorLink ? url : ''}`;
206
+ // Namespace without padding - keeps colors stable
207
+ // Editor link appended if enabled
208
+ namespace = `gg:${callpoint}${ggConfig.editorLink ? url : ''}`;
180
209
  }
181
210
  const ggLogFunction = namespaceToLogFunction.get(namespace) ||
182
- namespaceToLogFunction.set(namespace, debugFactory(namespace)).get(namespace);
211
+ namespaceToLogFunction.set(namespace, createGgDebugger(namespace)).get(namespace);
212
+ // Prepare args for logging
213
+ let logArgs;
214
+ let returnValue;
183
215
  if (!args.length) {
184
- ggLogFunction(` 📝📝 ${url} 👀👀`);
185
- return {
216
+ // No arguments: log editor link
217
+ logArgs = [` 📝📝 ${url} 👀👀`];
218
+ returnValue = {
186
219
  fileName,
187
220
  functionName,
188
221
  url,
189
222
  stack
190
223
  };
191
224
  }
192
- // Handle the case where args might be empty or have any number of arguments
193
- if (args.length === 1) {
194
- ggLogFunction(args[0]);
225
+ else if (args.length === 1) {
226
+ logArgs = [args[0]];
227
+ returnValue = args[0];
195
228
  }
196
229
  else {
197
- ggLogFunction(args[0], ...args.slice(1));
230
+ logArgs = [args[0], ...args.slice(1)];
231
+ returnValue = args[0];
232
+ }
233
+ // Log to console via debug
234
+ if (logArgs.length === 1) {
235
+ ggLogFunction(logArgs[0]);
236
+ }
237
+ else {
238
+ ggLogFunction(logArgs[0], ...logArgs.slice(1));
198
239
  }
199
240
  // Call capture hook if registered (for Eruda plugin)
200
- if (gg._onLog) {
201
- // Don't stringify args - keep them as-is for expandable objects
202
- const message = args.length === 1 ? String(args[0]) : args.map(String).join(' ');
203
- gg._onLog({
204
- namespace,
205
- color: ggLogFunction.color,
206
- diff: ggLogFunction.diff || 0, // Millisecond diff from debug library
207
- message,
208
- args, // Keep raw args for object inspection
209
- timestamp: Date.now()
210
- });
241
+ const entry = {
242
+ namespace,
243
+ color: ggLogFunction.color,
244
+ diff: ggLogFunction.diff || 0, // Millisecond diff from debug library
245
+ message: logArgs.length === 1 ? String(logArgs[0]) : logArgs.map(String).join(' '),
246
+ args: logArgs, // Keep raw args for object inspection
247
+ timestamp: Date.now()
248
+ };
249
+ if (_onLogCallback) {
250
+ _onLogCallback(entry);
211
251
  }
212
- return args[0];
252
+ else {
253
+ // Buffer early logs before Eruda initializes
254
+ earlyLogBuffer.push(entry);
255
+ }
256
+ return returnValue;
213
257
  }
214
258
  gg.disable = isCloudflareWorker() ? () => { } : debugFactory.disable;
215
259
  gg.enable = isCloudflareWorker() ? () => { } : debugFactory.enable;
@@ -232,21 +276,47 @@ gg.clearPersist = () => {
232
276
  * Hook for capturing gg() output (used by Eruda plugin)
233
277
  * Set this to a callback function to receive log entries
234
278
  */
235
- gg._onLog = null;
236
- // Log some gg info to the JS console/terminal:
237
- if (ggConfig.showHints && !isCloudflareWorker()) {
279
+ // Buffer for capturing early logs before Eruda initializes
280
+ const earlyLogBuffer = [];
281
+ let _onLogCallback = null;
282
+ // Proxy property that replays buffered logs when hook is registered
283
+ Object.defineProperty(gg, '_onLog', {
284
+ get() {
285
+ return _onLogCallback;
286
+ },
287
+ set(callback) {
288
+ _onLogCallback = callback;
289
+ // Replay buffered logs when callback is first registered
290
+ if (callback && earlyLogBuffer.length > 0) {
291
+ earlyLogBuffer.forEach((entry) => callback(entry));
292
+ earlyLogBuffer.length = 0; // Clear buffer after replay
293
+ }
294
+ }
295
+ });
296
+ // Namespace for adding properties to the gg function
297
+ // eslint-disable-next-line @typescript-eslint/no-namespace
298
+ (function (gg) {
299
+ })(gg || (gg = {}));
300
+ /**
301
+ * Run gg diagnostics and log configuration status
302
+ * Can be called immediately or delayed (e.g., after Eruda loads)
303
+ */
304
+ export async function runGgDiagnostics() {
305
+ if (!ggConfig.showHints || isCloudflareWorker())
306
+ return;
238
307
  const ggLogTest = debugFactory('gg:TEST');
239
308
  let ggMessage = '\n';
240
309
  // Utilities for forming ggMessage:
241
310
  const message = (s) => (ggMessage += `${s}\n`);
242
311
  const checkbox = (test) => (test ? '✅' : '❌');
243
312
  const makeHint = (test, ifTrue, ifFalse = '') => (test ? ifTrue : ifFalse);
313
+ // Use plain console.log for diagnostics - appears in Eruda's Console tab
244
314
  console.log(`Loaded gg module. Checking configuration...`);
245
315
  if (ggConfig.enabled && ggLogTest.enabled) {
246
316
  gg('If you can see this logg, gg configured correctly!');
247
317
  message(`No problems detected:`);
248
318
  if (BROWSER) {
249
- message(`ℹ️ If gg output still not visible above, enable "Verbose" log level in browser DevTools.`);
319
+ message(`ℹ️ If gg output not visible: enable "Verbose" log level in DevTools, or check Eruda's GG tab.`);
250
320
  }
251
321
  }
252
322
  else {
@@ -277,8 +347,18 @@ if (ggConfig.showHints && !isCloudflareWorker()) {
277
347
  }
278
348
  message(`${checkbox(ggLogTest.enabled)} DEBUG env variable: ${process?.env?.DEBUG}${hint}`);
279
349
  }
350
+ // Use plain console.log for diagnostics - appears in Eruda's Console tab
280
351
  console.log(ggMessage);
281
352
  // Reset namespace width after configuration check
282
353
  // This prevents the long callpoint from the config check from affecting subsequent logs
283
354
  resetNamespaceWidth();
284
355
  }
356
+ // Run diagnostics immediately on module load if Eruda is not being used
357
+ // (If Eruda will load, the loader will call runGgDiagnostics after Eruda is ready)
358
+ if (ggConfig.showHints && !isCloudflareWorker()) {
359
+ // Only run immediately if we're not in a context where Eruda might load
360
+ // In browser dev mode, assume Eruda might load and skip immediate diagnostics
361
+ if (!BROWSER || !DEV) {
362
+ runGgDiagnostics();
363
+ }
364
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leftium/gg",
3
- "version": "0.0.25",
3
+ "version": "0.0.27",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/Leftium/gg.git"