@leftium/gg 0.0.46 → 0.0.48

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/README.md CHANGED
@@ -6,7 +6,8 @@
6
6
  - Each namespace gets a unique color for easier visual parsing.
7
7
  - Simple syntax with wildcards to filter/hide debug output at runtime.
8
8
  - Millisecond diff (timestamps) for each namespace.
9
- - Can be inserted into the middle of expressions (returns the value of the first argument).
9
+ - Chainable API: `.ns()`, `.warn()`, `.error()`, `.info()`, `.trace()`, `.table()`.
10
+ - Can be inserted into the middle of expressions (use `.v` to get the passthrough value).
10
11
  - Can output a link that opens the source file in your editor (like VS Code).
11
12
  - Simple to disable (turn all loggs into NOP's for production).
12
13
  - Diagnostics/hints in dev console & terminal to help install and configure correctly.
@@ -28,8 +29,12 @@ npm add @leftium/gg
28
29
 
29
30
  gg('Hello world');
30
31
 
31
- // Log expressions (returns first argument)
32
- const result = gg(someFunction());
32
+ // Log with modifiers
33
+ gg('Connection timeout').warn();
34
+ gg('User authenticated', user).info();
35
+
36
+ // Passthrough with .v (returns the first argument)
37
+ const result = gg(someFunction()).v;
33
38
 
34
39
  // Multiple arguments
35
40
  gg('User:', user, 'Status:', status);
@@ -57,11 +62,10 @@ export default defineConfig({
57
62
 
58
63
  - **Call-sites plugin** -- rewrites `gg()` calls with source file/line/col metadata
59
64
  - **Open-in-editor plugin** -- adds dev server middleware for click-to-open
60
- - **Automatic `es2022` target** -- required for top-level await
61
65
 
62
66
  ### 3. Add the debug console (optional, recommended)
63
67
 
64
- An in-browser debug console (powered by Eruda) with a dedicated GG tab for filtering and inspecting logs especially useful on mobile.
68
+ An in-browser debug console (powered by Eruda) with a dedicated GG tab for filtering and inspecting logs -- especially useful on mobile.
65
69
 
66
70
  ```svelte
67
71
  <!-- src/routes/+layout.svelte -->
@@ -76,6 +80,173 @@ An in-browser debug console (powered by Eruda) with a dedicated GG tab for filte
76
80
  In development, the debug console appears automatically.
77
81
  In production, add `?gg` to the URL or use a 5-tap gesture to activate.
78
82
 
83
+ ## Chaining API
84
+
85
+ `gg()` returns a `GgChain<T>` with composable modifiers. Chain any combination, in any order. The log auto-flushes on the next microtask, or use `.v` to flush immediately and get the passthrough value.
86
+
87
+ ```javascript
88
+ import { gg } from '@leftium/gg';
89
+
90
+ // Basic logging (auto-flushes on microtask)
91
+ gg('hello');
92
+ gg('multiple', 'args', { data: 42 });
93
+
94
+ // Passthrough with .v (flushes immediately, returns first arg)
95
+ const result = gg(computeValue()).v;
96
+ const user = gg(await fetchUser()).ns('api').v;
97
+
98
+ // Log levels
99
+ gg('System ready').info(); // blue indicator
100
+ gg('Deprecated API call').warn(); // yellow indicator
101
+ gg('Connection failed').error(); // red indicator + stack trace
102
+
103
+ // Custom namespace
104
+ gg('Processing request').ns('api:handler');
105
+
106
+ // Stack trace
107
+ gg('Debug checkpoint').trace();
108
+
109
+ // Table formatting (also emits native console.table)
110
+ gg(arrayOfObjects).table();
111
+ gg(data).table(['name', 'age']); // filter columns
112
+
113
+ // Combine modifiers freely
114
+ gg('Slow query', { ms: 3200 }).ns('db').warn();
115
+ const rows = gg(queryResult).ns('db:query').table().v;
116
+ ```
117
+
118
+ ### `.v` -- Passthrough
119
+
120
+ Use `.v` at the end of any chain to flush the log immediately and return the first argument. This lets you insert `gg()` into the middle of expressions:
121
+
122
+ ```javascript
123
+ // Without .v: logs on microtask, returns GgChain (not the value!)
124
+ gg(someValue);
125
+
126
+ // With .v: logs immediately, returns someValue
127
+ const x = gg(someValue).v;
128
+ const y = gg(compute()).ns('math').warn().v;
129
+
130
+ // Insert into expressions
131
+ processData(gg(inputData).v);
132
+ return gg(result).ns('output').v;
133
+ ```
134
+
135
+ ### `.ns(label)` -- Custom Namespace
136
+
137
+ Override the auto-generated namespace (file@function) with a custom label. Useful for grouping related logs across files:
138
+
139
+ ```javascript
140
+ gg('Request received').ns('api:incoming');
141
+ gg('Response sent').ns('api:outgoing');
142
+ gg('Cache hit').ns('api:cache');
143
+ ```
144
+
145
+ Namespace labels support **template variables** that resolve from plugin-provided metadata:
146
+
147
+ | Variable | Description | Example |
148
+ | -------- | ----------------------------- | --------------------------------- |
149
+ | `$NS` | Full auto-generated callpoint | `routes/+page.svelte@handleClick` |
150
+ | `$FN` | Enclosing function name | `handleClick` |
151
+ | `$FILE` | Source file path | `routes/+page.svelte` |
152
+ | `$LINE` | Line number | `42` |
153
+ | `$COL` | Column number | `3` |
154
+
155
+ ```javascript
156
+ gg('debug info').ns('ERROR:$NS'); // → ERROR:routes/+page.svelte@handleClick
157
+ gg('validation').ns('$FILE:validate'); // → routes/+page.svelte:validate
158
+ gg('step 1').ns('TRACE:$FN'); // → TRACE:handleClick
159
+ gg('context').ns('$NS:debug'); // → routes/+page.svelte@handleClick:debug
160
+ ```
161
+
162
+ Without the Vite plugin, `$NS` falls back to a runtime word-tuple (e.g. `calm-fox`). `$FN`, `$FILE`, `$LINE`, and `$COL` require the plugin.
163
+
164
+ ### `.info()` / `.warn()` / `.error()` -- Log Levels
165
+
166
+ ```javascript
167
+ gg('Server started on port 3000').info(); // blue badge
168
+ gg('Rate limit approaching').warn(); // yellow badge
169
+ gg('Unhandled exception').error(); // red badge + captures stack
170
+
171
+ // .error() with an Error object uses its .stack
172
+ try {
173
+ riskyOperation();
174
+ } catch (err) {
175
+ gg(err).error();
176
+ }
177
+ ```
178
+
179
+ ### `.trace()` -- Stack Trace
180
+
181
+ Captures a full stack trace (cleaned of internal gg frames) alongside the log entry:
182
+
183
+ ```javascript
184
+ gg('How did we get here?').trace();
185
+ ```
186
+
187
+ ### `.table(columns?)` -- Table Formatting
188
+
189
+ Formats the first argument as a table. Also emits a native `console.table()` call. Optionally filter columns:
190
+
191
+ ```javascript
192
+ gg([
193
+ { name: 'Alice', age: 30, role: 'admin' },
194
+ { name: 'Bob', age: 25, role: 'user' }
195
+ ]).table();
196
+
197
+ // Filter columns
198
+ gg(users).table(['name', 'role']);
199
+
200
+ // Works with objects-of-objects and arrays of primitives too
201
+ gg({ us: { pop: '331M' }, uk: { pop: '67M' } }).table();
202
+ gg(['apple', 'banana', 'cherry']).table();
203
+ ```
204
+
205
+ ## Timers
206
+
207
+ Measure elapsed time with `gg.time()`, `gg.timeLog()`, and `gg.timeEnd()`:
208
+
209
+ ```javascript
210
+ import { gg } from '@leftium/gg';
211
+
212
+ gg.time('fetch');
213
+
214
+ // ... some work ...
215
+ gg.timeLog('fetch', 'headers received'); // logs elapsed without stopping
216
+
217
+ // ... more work ...
218
+ gg.timeEnd('fetch'); // logs elapsed and stops timer
219
+ ```
220
+
221
+ ### Timer Namespaces
222
+
223
+ `gg.time()` returns a `GgTimerChain` that supports `.ns()` for grouping. The namespace is inherited by subsequent `timeLog` and `timeEnd` calls for the same label:
224
+
225
+ ```javascript
226
+ gg.time('fetch').ns('api-pipeline');
227
+
228
+ gg.timeLog('fetch', 'step 1 done'); // logged under 'api-pipeline'
229
+ gg.timeEnd('fetch'); // logged under 'api-pipeline'
230
+
231
+ // Template variables work too
232
+ gg.time('db-query').ns('$FN:timers');
233
+ ```
234
+
235
+ ## `gg.here()` -- Open in Editor
236
+
237
+ Returns call-site metadata for rendering "open in editor" links. Replaces the old no-arg `gg()` introspection.
238
+
239
+ ```svelte
240
+ <script>
241
+ import { gg } from '@leftium/gg';
242
+ </script>
243
+
244
+ <!-- Pass to a link component -->
245
+ <OpenInEditorLink gg={gg.here()} />
246
+ ```
247
+
248
+ Returns `{ fileName, functionName, url }` where `url` points to the dev server's open-in-editor endpoint.
249
+
79
250
  ## GgConsole Options
80
251
 
81
252
  ```svelte
@@ -226,6 +397,40 @@ initGgEruda();
226
397
  gg('works in any framework');
227
398
  ```
228
399
 
400
+ ## API Reference
401
+
402
+ ### `gg(value, ...args)` -- Returns `GgChain<T>`
403
+
404
+ | Method / Property | Description |
405
+ | ----------------- | --------------------------------------------- |
406
+ | `.v` | Flush log immediately, return first argument |
407
+ | `.ns(label)` | Set custom namespace (supports template vars) |
408
+ | `.info()` | Set log level to info |
409
+ | `.warn()` | Set log level to warn |
410
+ | `.error()` | Set log level to error (captures stack) |
411
+ | `.trace()` | Attach full stack trace |
412
+ | `.table(cols?)` | Format as table, optional column filter |
413
+
414
+ ### `gg.time(label?)` -- Returns `GgTimerChain`
415
+
416
+ | Method | Description |
417
+ | ------------ | ----------------------------------------- |
418
+ | `.ns(label)` | Set namespace for timer group (inherited) |
419
+
420
+ ### `gg.timeLog(label?, ...args)` -- Log elapsed without stopping
421
+
422
+ ### `gg.timeEnd(label?)` -- Log elapsed and stop timer
423
+
424
+ ### `gg.here()` -- Returns `{ fileName, functionName, url }`
425
+
426
+ ### Control
427
+
428
+ | Method | Description |
429
+ | ------------------- | ----------------------------------------- |
430
+ | `gg.enable(ns)` | Enable debug output for namespace pattern |
431
+ | `gg.disable()` | Disable all debug output |
432
+ | `gg.clearPersist()` | Clear `gg-enabled` from localStorage |
433
+
229
434
  ## Technical Details
230
435
 
231
436
  ### Internal Debug Implementation
@@ -248,6 +453,20 @@ Features implemented internally (~290 lines of TypeScript):
248
453
 
249
454
  This approach eliminates the need for vendoring, patching, and bundling third-party code, resulting in better type safety and simpler maintenance.
250
455
 
456
+ ### Microtask Auto-Flush
457
+
458
+ When you call `gg(value)`, the log is **deferred to the next microtask**. This means chain modifiers (`.ns()`, `.warn()`, etc.) can be added synchronously after the call. If you need the value immediately (passthrough), use `.v` which forces an immediate flush.
459
+
460
+ ```javascript
461
+ // These two are equivalent:
462
+ gg('hello').warn(); // .warn() runs sync, log flushes on microtask
463
+ gg('hello').warn().v; // .v forces immediate flush + returns 'hello'
464
+
465
+ // Auto-flush means order doesn't matter for modifiers:
466
+ gg('x').ns('foo').warn(); // same as:
467
+ gg('x').warn().ns('foo'); // (both set ns and level before flush)
468
+ ```
469
+
251
470
  ## Inspirations
252
471
 
253
472
  ### debug
@@ -90,16 +90,19 @@ export declare function findEnclosingFunctionFromScopes(pos: number, scopes: Fun
90
90
  */
91
91
  export declare function escapeForString(s: string): string;
92
92
  /**
93
- * Transform gg() and gg.ns() calls in source code to gg._ns({ns, file, line, col, src}, ...) calls.
93
+ * Transform gg() calls in source code to inject call-site metadata.
94
94
  *
95
95
  * Handles:
96
96
  * - bare gg(expr) → gg._ns({ns, file, line, col, src: 'expr'}, expr)
97
- * - gg.ns('label', expr) → gg._ns({ns, file, line, col, src: 'expr'}, expr)
98
- * - label supports template variables: $NS, $FN, $FILE, $LINE, $COL
99
- * - plain label (no variables) is used as-is (no auto @fn append)
100
- * - gg.enable, gg.disable, gg.clearPersist, gg._onLog, gg._ns → left untouched
97
+ * - gg.here() → gg._here({ns, file, line, col})
98
+ * - gg.time/timeLog/timeEnd gg._time/_timeLog/_timeEnd with metadata
99
+ * - gg.enable, gg.disable, gg.clearPersist, gg._ns, gg._onLog left untouched
101
100
  * - gg inside strings and comments → left untouched
102
101
  *
102
+ * Chain methods (.ns(), .warn(), .error(), etc.) are NOT rewritten —
103
+ * they run at runtime and resolve template variables from the metadata
104
+ * that the plugin baked into the gg._ns() options object.
105
+ *
103
106
  * For .svelte files, `svelteInfo` (from `collectCodeRanges()`) determines which
104
107
  * positions contain JS code and provides AST-based function scope detection.
105
108
  * Script ranges use `{...}` object literal syntax; template ranges use `gg._o()`
@@ -47,13 +47,7 @@ export default function ggCallSitesPlugin(options = {}) {
47
47
  return null;
48
48
  // Quick bail: no gg calls in this file
49
49
  if (!code.includes('gg(') &&
50
- !code.includes('gg.ns(') &&
51
- !code.includes('gg.info(') &&
52
- !code.includes('gg.warn(') &&
53
- !code.includes('gg.error(') &&
54
- !code.includes('gg.table(') &&
55
- !code.includes('gg.trace(') &&
56
- !code.includes('gg.assert(') &&
50
+ !code.includes('gg.here(') &&
57
51
  !code.includes('gg.time(') &&
58
52
  !code.includes('gg.timeLog(') &&
59
53
  !code.includes('gg.timeEnd('))
@@ -590,16 +584,19 @@ export function escapeForString(s) {
590
584
  return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n').replace(/\r/g, '\\r');
591
585
  }
592
586
  /**
593
- * Transform gg() and gg.ns() calls in source code to gg._ns({ns, file, line, col, src}, ...) calls.
587
+ * Transform gg() calls in source code to inject call-site metadata.
594
588
  *
595
589
  * Handles:
596
590
  * - bare gg(expr) → gg._ns({ns, file, line, col, src: 'expr'}, expr)
597
- * - gg.ns('label', expr) → gg._ns({ns, file, line, col, src: 'expr'}, expr)
598
- * - label supports template variables: $NS, $FN, $FILE, $LINE, $COL
599
- * - plain label (no variables) is used as-is (no auto @fn append)
600
- * - gg.enable, gg.disable, gg.clearPersist, gg._onLog, gg._ns → left untouched
591
+ * - gg.here() → gg._here({ns, file, line, col})
592
+ * - gg.time/timeLog/timeEnd gg._time/_timeLog/_timeEnd with metadata
593
+ * - gg.enable, gg.disable, gg.clearPersist, gg._ns, gg._onLog left untouched
601
594
  * - gg inside strings and comments → left untouched
602
595
  *
596
+ * Chain methods (.ns(), .warn(), .error(), etc.) are NOT rewritten —
597
+ * they run at runtime and resolve template variables from the metadata
598
+ * that the plugin baked into the gg._ns() options object.
599
+ *
603
600
  * For .svelte files, `svelteInfo` (from `collectCodeRanges()`) determines which
604
601
  * positions contain JS code and provides AST-based function scope detection.
605
602
  * Script ranges use `{...}` object literal syntax; template ranges use `gg._o()`
@@ -720,7 +717,7 @@ export function transformGgCalls(code, shortPath, filePath, svelteInfo, jsFuncti
720
717
  continue;
721
718
  }
722
719
  }
723
- // Look for 'gg' pattern — could be gg( or gg.ns(
720
+ // Look for 'gg' pattern — could be gg( or gg.here( or gg.time(
724
721
  if (code[i] === 'g' && code[i + 1] === 'g') {
725
722
  // In .svelte files, skip gg outside code ranges (prose text, etc.)
726
723
  const range = rangeAt(i);
@@ -734,83 +731,22 @@ export function transformGgCalls(code, shortPath, filePath, svelteInfo, jsFuncti
734
731
  i++;
735
732
  continue;
736
733
  }
737
- // Case 1: gg.ns('label', ...) → gg._ns({ns: 'label', file, line, col, src}, ...)
738
- if (code.slice(i + 2, i + 6) === '.ns(') {
734
+ // Case 1: gg.here() → gg._here({ns, file, line, col})
735
+ if (code.slice(i + 2, i + 9) === '.here()') {
739
736
  const { line, col } = getLineCol(code, i);
740
737
  const fnName = getFunctionName(i, range);
741
- const openParenPos = i + 5; // position of '(' in 'gg.ns('
742
- // Find matching closing paren for the entire gg.ns(...) call
743
- const closeParenPos = findMatchingParen(code, openParenPos);
744
- if (closeParenPos === -1) {
745
- i += 6;
746
- continue;
747
- }
748
- // Extract the first argument (the namespace string)
749
- // Look for the string literal after 'gg.ns('
750
- let afterNsParen = i + 6; // position after 'gg.ns('
751
- while (afterNsParen < code.length && /\s/.test(code[afterNsParen]))
752
- afterNsParen++;
753
- const quoteChar = code[afterNsParen];
754
- if (quoteChar === "'" || quoteChar === '"') {
755
- // Find the closing quote
756
- let j = afterNsParen + 1;
757
- while (j < code.length && code[j] !== quoteChar) {
758
- if (code[j] === '\\')
759
- j++; // skip escaped chars
760
- j++;
761
- }
762
- // j now points to closing quote
763
- const nsLabelRaw = code.slice(afterNsParen + 1, j);
764
- // Build callpoint: substitute $NS/$FN/$FILE/$LINE/$COL template variables.
765
- // The auto-generated callpoint (file@fn) is what bare gg() would produce.
766
- const autoCallpoint = `${shortPath}${fnName ? `@${fnName}` : ''}`;
767
- const callpoint = escapeForString(nsLabelRaw
768
- .replace(/\$NS/g, autoCallpoint)
769
- .replace(/\$FN/g, fnName)
770
- .replace(/\$FILE/g, shortPath)
771
- .replace(/\$LINE/g, String(line))
772
- .replace(/\$COL/g, String(col)));
773
- // Check if there are more args after the string
774
- const afterClosingQuote = j + 1;
775
- let k = afterClosingQuote;
776
- while (k < code.length && /\s/.test(code[k]))
777
- k++;
778
- if (code[k] === ')') {
779
- // gg.ns('label') → gg._ns(opts)
780
- result.push(code.slice(lastIndex, i));
781
- result.push(`gg._ns(${buildOptions(range, callpoint, line, col)})`);
782
- lastIndex = k + 1;
783
- i = k + 1;
784
- }
785
- else if (code[k] === ',') {
786
- // gg.ns('label', args...) → gg._ns(opts, args...)
787
- let argsStart = k + 1;
788
- while (argsStart < closeParenPos && /\s/.test(code[argsStart]))
789
- argsStart++;
790
- const argsSrc = code.slice(argsStart, closeParenPos).trim();
791
- const escapedSrc = escapeForString(argsSrc);
792
- result.push(code.slice(lastIndex, i));
793
- result.push(`gg._ns(${buildOptions(range, callpoint, line, col, escapedSrc)}, `);
794
- lastIndex = k + 1; // skip past the comma, keep args as-is
795
- i = k + 1;
796
- }
797
- else {
798
- // Unexpected — leave untouched
799
- i += 6;
800
- continue;
801
- }
802
- modified = true;
803
- continue;
804
- }
805
- // Non-string first arg to gg.ns — skip (can't extract ns at build time)
806
- i += 6;
738
+ const callpoint = `${shortPath}${fnName ? `@${fnName}` : ''}`;
739
+ const escapedNs = escapeForString(callpoint);
740
+ result.push(code.slice(lastIndex, i));
741
+ result.push(`gg._here(${buildOptions(range, escapedNs, line, col)})`);
742
+ lastIndex = i + 9; // skip past 'gg.here()'
743
+ i = lastIndex;
744
+ modified = true;
807
745
  continue;
808
746
  }
809
- // Case 1b: gg.info/warn/error/table/trace/assert → gg._info/_warn/_error/_table/_trace/_assert
810
- // These methods are rewritten like bare gg() but with their internal variant.
811
- const dotMethodMatch = code
812
- .slice(i + 2)
813
- .match(/^\.(info|warn|error|table|trace|assert|time|timeLog|timeEnd)\(/);
747
+ // Case 2: gg.time/timeLog/timeEnd → gg._time/_timeLog/_timeEnd
748
+ // Timer methods are rewritten to inject call-site metadata.
749
+ const dotMethodMatch = code.slice(i + 2).match(/^\.(time|timeLog|timeEnd)\(/);
814
750
  if (dotMethodMatch) {
815
751
  const methodName = dotMethodMatch[1];
816
752
  const internalName = `_${methodName}`;
@@ -828,13 +764,13 @@ export function transformGgCalls(code, shortPath, filePath, svelteInfo, jsFuncti
828
764
  const argsText = code.slice(openParenPos + 1, closeParenPos).trim();
829
765
  result.push(code.slice(lastIndex, i));
830
766
  if (argsText === '') {
831
- // gg.warn() → gg._warn(opts)
767
+ // gg.time() → gg._time(opts)
832
768
  result.push(`gg.${internalName}(${buildOptions(range, escapedNs, line, col)})`);
833
769
  lastIndex = closeParenPos + 1;
834
770
  i = closeParenPos + 1;
835
771
  }
836
772
  else {
837
- // gg.warn(expr) → gg._warn(opts, expr)
773
+ // gg.time('label') → gg._time(opts, 'label')
838
774
  const escapedSrc = escapeForString(argsText);
839
775
  result.push(`gg.${internalName}(${buildOptions(range, escapedNs, line, col, escapedSrc)}, `);
840
776
  lastIndex = openParenPos + 1; // keep original args
@@ -843,12 +779,12 @@ export function transformGgCalls(code, shortPath, filePath, svelteInfo, jsFuncti
843
779
  modified = true;
844
780
  continue;
845
781
  }
846
- // Skip other gg.* calls (gg.enable, gg.disable, gg._ns, gg._onLog, gg.time, etc.)
782
+ // Skip other gg.* calls (gg.enable, gg.disable, gg._ns, gg._onLog, etc.)
847
783
  if (code[i + 2] === '.') {
848
784
  i += 3;
849
785
  continue;
850
786
  }
851
- // Case 2: bare gg(...) → gg._ns({ns, file, line, col, src}, ...)
787
+ // Case 3: bare gg(...) → gg._ns({ns, file, line, col, src}, ...)
852
788
  if (code[i + 2] === '(') {
853
789
  const { line, col } = getLineCol(code, i);
854
790
  const fnName = getFunctionName(i, range);
package/dist/gg.d.ts CHANGED
@@ -21,16 +21,91 @@ interface CapturedEntry {
21
21
  };
22
22
  }
23
23
  type OnLogCallback = (entry: CapturedEntry) => void;
24
- export declare function gg(): {
25
- fileName: string;
26
- functionName: string;
27
- url: string;
28
- };
29
- export declare function gg<T>(arg: T, ...args: unknown[]): T;
30
- export declare namespace gg {
31
- var disable: () => string;
32
- var enable: (ns: string) => void;
33
- var clearPersist: () => void;
24
+ /**
25
+ * Log a value and return a chainable wrapper.
26
+ *
27
+ * Chain modifiers to configure the log entry:
28
+ * - `.ns('label')` — set a custom namespace
29
+ * - `.warn()` / `.error()` / `.info()` — set log level
30
+ * - `.trace()` — include stack trace
31
+ * - `.table()` format as ASCII table
32
+ * - `.v` flush immediately and return the passthrough value
33
+ *
34
+ * Without `.v`, the log auto-flushes on the next microtask.
35
+ *
36
+ * @example
37
+ * gg(value) // log with auto namespace
38
+ * gg(value).ns('label').warn() // log with namespace + warn level
39
+ * const x = gg(value).v // passthrough
40
+ * const x = gg(value).ns('foo').v // passthrough with namespace
41
+ */
42
+ export declare function gg<T>(arg: T, ...args: unknown[]): GgChain<T>;
43
+ /** Internal options for the core log function */
44
+ interface LogOptions {
45
+ ns: string;
46
+ file?: string;
47
+ line?: number;
48
+ col?: number;
49
+ src?: string;
50
+ level?: LogLevel;
51
+ stack?: string;
52
+ tableData?: {
53
+ keys: string[];
54
+ rows: Array<Record<string, unknown>>;
55
+ };
56
+ }
57
+ /**
58
+ * Chainable wrapper returned by gg(). Collects modifiers (.ns(), .warn(), etc.)
59
+ * and auto-flushes the log on the next microtask. Use `.v` to flush immediately
60
+ * and get the passthrough value.
61
+ *
62
+ * @example
63
+ * gg(value) // logs on microtask
64
+ * gg(value).ns('label').warn() // logs with namespace + warn level
65
+ * const x = gg(value).v // logs immediately, returns value
66
+ * const x = gg(value).ns('foo').v // logs with namespace, returns value
67
+ */
68
+ export declare class GgChain<T> {
69
+ #private;
70
+ constructor(value: T, args: unknown[], options: LogOptions, disabled?: boolean);
71
+ /** Set a custom namespace for this log entry.
72
+ *
73
+ * Supports template variables (resolved from plugin-provided metadata):
74
+ * $NS - auto-generated callpoint (file@fn with plugin, word-tuple without)
75
+ * $FN - enclosing function name (extracted from $NS)
76
+ * $FILE - short file path (extracted from $NS)
77
+ * $LINE - line number
78
+ * $COL - column number
79
+ */
80
+ ns(label: string): GgChain<T>;
81
+ /** Set log level to info (blue indicator). */
82
+ info(): GgChain<T>;
83
+ /** Set log level to warn (yellow indicator). */
84
+ warn(): GgChain<T>;
85
+ /** Set log level to error (red indicator, captures stack trace). */
86
+ error(): GgChain<T>;
87
+ /** Include a full stack trace with this log entry. */
88
+ trace(): GgChain<T>;
89
+ /** Format the log output as an ASCII table. */
90
+ table(columns?: string[]): GgChain<T>;
91
+ /** Flush the log immediately and return the passthrough value. */
92
+ get v(): T;
93
+ }
94
+ /**
95
+ * Chainable wrapper returned by gg.time(). Only supports .ns() for setting
96
+ * the namespace for the entire timer group (inherited by timeLog/timeEnd).
97
+ *
98
+ * @example
99
+ * gg.time('fetch').ns('api-pipeline')
100
+ * gg.time('fetch').ns('$FN:timers') // template vars work too
101
+ */
102
+ export declare class GgTimerChain {
103
+ #private;
104
+ constructor(label: string, options: LogOptions);
105
+ /** Set a custom namespace for this timer group.
106
+ * Supports the same template variables as GgChain.ns().
107
+ */
108
+ ns(label: string): GgTimerChain;
34
109
  }
35
110
  /**
36
111
  * ANSI Color Helpers for gg()
@@ -134,7 +209,6 @@ export declare function underline(): ChainableColorFn;
134
209
  export declare function dim(): ChainableColorFn;
135
210
  export declare namespace gg {
136
211
  let _onLog: OnLogCallback | null;
137
- let ns: (nsLabel: string, ...args: unknown[]) => unknown;
138
212
  let _ns: (options: {
139
213
  ns: string;
140
214
  file?: string;
@@ -143,7 +217,7 @@ export declare namespace gg {
143
217
  src?: string;
144
218
  level?: LogLevel;
145
219
  stack?: string;
146
- }, ...args: unknown[]) => unknown;
220
+ }, ...args: unknown[]) => GgChain<unknown>;
147
221
  let _o: (ns: string, file?: string, line?: number, col?: number, src?: string) => {
148
222
  ns: string;
149
223
  file?: string;
@@ -151,64 +225,34 @@ export declare namespace gg {
151
225
  col?: number;
152
226
  src?: string;
153
227
  };
154
- let info: (...args: unknown[]) => unknown;
155
- let warn: (...args: unknown[]) => unknown;
156
- let error: (...args: unknown[]) => unknown;
157
- let assert: (condition: unknown, ...args: unknown[]) => unknown;
158
- let table: (data: unknown, columns?: string[]) => unknown;
159
- let time: (label?: string) => void;
160
- let timeLog: (label?: string, ...args: unknown[]) => void;
161
- let timeEnd: (label?: string) => void;
162
- let trace: (...args: unknown[]) => unknown;
163
- let _info: (options: {
164
- ns: string;
165
- file?: string;
166
- line?: number;
167
- col?: number;
168
- src?: string;
169
- }, ...args: unknown[]) => unknown;
170
- let _warn: (options: {
171
- ns: string;
172
- file?: string;
173
- line?: number;
174
- col?: number;
175
- src?: string;
176
- }, ...args: unknown[]) => unknown;
177
- let _error: (options: {
178
- ns: string;
179
- file?: string;
180
- line?: number;
181
- col?: number;
182
- src?: string;
183
- }, ...args: unknown[]) => unknown;
184
- let _assert: (options: {
185
- ns: string;
186
- file?: string;
187
- line?: number;
188
- col?: number;
189
- src?: string;
190
- }, condition: unknown, ...args: unknown[]) => unknown;
191
- let _table: (options: {
192
- ns: string;
193
- file?: string;
194
- line?: number;
195
- col?: number;
196
- src?: string;
197
- }, data: unknown, columns?: string[]) => unknown;
198
- let _trace: (options: {
228
+ let here: () => {
229
+ fileName: string;
230
+ functionName: string;
231
+ url: string;
232
+ };
233
+ let _here: (options: {
199
234
  ns: string;
200
235
  file?: string;
201
236
  line?: number;
202
237
  col?: number;
203
- src?: string;
204
- }, ...args: unknown[]) => unknown;
238
+ }) => {
239
+ fileName: string;
240
+ functionName: string;
241
+ url: string;
242
+ };
243
+ let enable: (ns: string) => void;
244
+ let disable: () => string;
245
+ let clearPersist: () => void;
246
+ let time: (label?: string) => GgTimerChain;
247
+ let timeLog: (label?: string, ...args: unknown[]) => void;
248
+ let timeEnd: (label?: string) => void;
205
249
  let _time: (options: {
206
250
  ns: string;
207
251
  file?: string;
208
252
  line?: number;
209
253
  col?: number;
210
254
  src?: string;
211
- }, label?: string) => void;
255
+ }, label?: string) => GgTimerChain;
212
256
  let _timeLog: (options: {
213
257
  ns: string;
214
258
  file?: string;
package/dist/gg.js CHANGED
@@ -1,4 +1,4 @@
1
- import debugFactory, {} from './debug/index.js';
1
+ import debugFactory, { debugReady } from './debug/index.js';
2
2
  import { BROWSER, DEV } from 'esm-env';
3
3
  import { toWordTuple } from './words.js';
4
4
  const _ggCallSitesPlugin = typeof __GG_TAG_PLUGIN__ !== 'undefined' ? __GG_TAG_PLUGIN__ : false;
@@ -221,7 +221,8 @@ function openInEditorUrl(fileName, line, col) {
221
221
  }
222
222
  export function gg(...args) {
223
223
  if (!ggConfig.enabled || isCloudflareWorker()) {
224
- return args.length ? args[0] : { fileName: '', functionName: '', url: '' };
224
+ // Return a no-op chain that skips logging
225
+ return new GgChain(args[0], args, { ns: '' }, true);
225
226
  }
226
227
  // Without the call-sites plugin, use cheap stack hash → deterministic word tuple.
227
228
  // When the plugin IS installed, all gg() calls are rewritten to gg._ns() at build time,
@@ -229,49 +230,166 @@ export function gg(...args) {
229
230
  // Same call site always produces the same word pair (e.g. "calm-fox").
230
231
  // depth=2: skip "Error" header [0] and gg() frame [1]
231
232
  const callpoint = resolveCallpoint(2);
232
- return ggLog({ ns: callpoint }, ...args);
233
+ return new GgChain(args[0], args, { ns: callpoint });
233
234
  }
234
235
  /**
235
- * gg.ns() - Log with an explicit namespace (callpoint label).
236
- *
237
- * Users call gg.ns() directly to set a meaningful label that survives
238
- * across builds. For the internal plugin-generated version with file
239
- * metadata, see gg._ns().
240
- *
241
- * The label supports template variables (substituted by the vite plugin
242
- * at build time, or at runtime for $NS):
243
- * $NS - auto-generated callpoint (file@fn with plugin, word-tuple without)
244
- * $FN - enclosing function name (plugin only, empty without)
245
- * $FILE - short file path (plugin only, empty without)
246
- * $LINE - line number (plugin only, empty without)
247
- * $COL - column number (plugin only, empty without)
236
+ * gg.here() - Return call-site info for open-in-editor.
248
237
  *
249
- * @param nsLabel - The namespace label (appears as gg:<nsLabel> in output)
250
- * @param args - Same arguments as gg()
251
- * @returns Same as gg() - the first arg, or call-site info if no args
238
+ * Replaces the old no-arg gg() overload. Returns an object with the
239
+ * file name, function name, and URL for opening the source in an editor.
252
240
  *
253
241
  * @example
254
- * gg.ns("auth", "login failed") // → gg:auth
255
- * gg.ns("ERROR:$NS", msg) // → gg:ERROR:routes/+page.svelte@handleClick (with plugin)
256
- * // → gg:ERROR:calm-fox (without plugin)
257
- * gg.ns("$NS:validation", fieldName) // → gg:routes/+page.svelte@handleClick:validation
242
+ * <OpenInEditorLink gg={gg.here()} />
258
243
  */
259
- gg.ns = function (nsLabel, ...args) {
260
- // Resolve $NS at runtime (word-tuple fallback when plugin isn't installed).
261
- // With the plugin, $NS is already substituted at build time before this runs.
262
- // depth=3: skip "Error" [0], resolveCallpoint [1], gg.ns [2] → caller [3]
263
- if (nsLabel.includes('$NS')) {
264
- const callpoint = resolveCallpoint(3);
265
- nsLabel = nsLabel.replace(/\$NS/g, callpoint);
244
+ gg.here = function () {
245
+ if (!ggConfig.enabled || isCloudflareWorker()) {
246
+ return { fileName: '', functionName: '', url: '' };
266
247
  }
267
- return gg._ns({ ns: nsLabel }, ...args);
248
+ const callpoint = resolveCallpoint(3);
249
+ const namespace = `gg:${callpoint}`;
250
+ // Log the call-site info
251
+ const ggLogFunction = namespaceToLogFunction.get(namespace) ||
252
+ namespaceToLogFunction.set(namespace, createGgDebugger(namespace)).get(namespace);
253
+ ggLogFunction(` 📝 ${callpoint}`);
254
+ return {
255
+ fileName: callpoint,
256
+ functionName: callpoint.includes('@') ? callpoint.split('@').pop() || '' : '',
257
+ url: ''
258
+ };
268
259
  };
260
+ /**
261
+ * Resolve template variables in a namespace label using metadata from the plugin.
262
+ *
263
+ * The Vite plugin bakes the auto-generated callpoint into options.ns at build time
264
+ * (e.g. "routes/+page.svelte@handleClick"). This function extracts components from
265
+ * that callpoint and substitutes template variables:
266
+ *
267
+ * $NS - the full auto-generated callpoint (or runtime word-tuple fallback)
268
+ * $FN - the function name portion (after @)
269
+ * $FILE - the file path portion (before @)
270
+ * $LINE - the line number
271
+ * $COL - the column number
272
+ */
273
+ function resolveNsTemplateVars(label, options) {
274
+ if (!label.includes('$'))
275
+ return label;
276
+ const ns = options.ns || '';
277
+ // $NS: use the full auto-generated callpoint. If no plugin, fall back to runtime stack hash.
278
+ if (label.includes('$NS')) {
279
+ const callpoint = ns || resolveCallpoint(4);
280
+ label = label.replace(/\$NS/g, callpoint);
281
+ }
282
+ // $FN: extract function name from "file@fn" format
283
+ if (label.includes('$FN')) {
284
+ const fn = ns.includes('@') ? ns.split('@').pop() || '' : '';
285
+ label = label.replace(/\$FN/g, fn);
286
+ }
287
+ // $FILE: extract file path from "file@fn" format
288
+ if (label.includes('$FILE')) {
289
+ const file = ns.includes('@') ? ns.split('@')[0] : ns;
290
+ label = label.replace(/\$FILE/g, file);
291
+ }
292
+ // $LINE / $COL: from plugin metadata
293
+ if (label.includes('$LINE')) {
294
+ label = label.replace(/\$LINE/g, String(options.line ?? ''));
295
+ }
296
+ if (label.includes('$COL')) {
297
+ label = label.replace(/\$COL/g, String(options.col ?? ''));
298
+ }
299
+ return label;
300
+ }
301
+ /**
302
+ * Chainable wrapper returned by gg(). Collects modifiers (.ns(), .warn(), etc.)
303
+ * and auto-flushes the log on the next microtask. Use `.v` to flush immediately
304
+ * and get the passthrough value.
305
+ *
306
+ * @example
307
+ * gg(value) // logs on microtask
308
+ * gg(value).ns('label').warn() // logs with namespace + warn level
309
+ * const x = gg(value).v // logs immediately, returns value
310
+ * const x = gg(value).ns('foo').v // logs with namespace, returns value
311
+ */
312
+ export class GgChain {
313
+ #value;
314
+ #args;
315
+ #options;
316
+ #flushed = false;
317
+ #disabled;
318
+ constructor(value, args, options, disabled = false) {
319
+ this.#value = value;
320
+ this.#args = args;
321
+ this.#options = options;
322
+ this.#disabled = disabled;
323
+ if (!disabled) {
324
+ // Auto-flush on microtask if not flushed synchronously by .v or another trigger
325
+ queueMicrotask(() => this.#flush());
326
+ }
327
+ }
328
+ /** Set a custom namespace for this log entry.
329
+ *
330
+ * Supports template variables (resolved from plugin-provided metadata):
331
+ * $NS - auto-generated callpoint (file@fn with plugin, word-tuple without)
332
+ * $FN - enclosing function name (extracted from $NS)
333
+ * $FILE - short file path (extracted from $NS)
334
+ * $LINE - line number
335
+ * $COL - column number
336
+ */
337
+ ns(label) {
338
+ this.#options.ns = resolveNsTemplateVars(label, this.#options);
339
+ return this;
340
+ }
341
+ /** Set log level to info (blue indicator). */
342
+ info() {
343
+ this.#options.level = 'info';
344
+ return this;
345
+ }
346
+ /** Set log level to warn (yellow indicator). */
347
+ warn() {
348
+ this.#options.level = 'warn';
349
+ return this;
350
+ }
351
+ /** Set log level to error (red indicator, captures stack trace). */
352
+ error() {
353
+ this.#options.level = 'error';
354
+ this.#options.stack = getErrorStack(this.#args[0], 3);
355
+ return this;
356
+ }
357
+ /** Include a full stack trace with this log entry. */
358
+ trace() {
359
+ this.#options.stack = captureStack(3);
360
+ return this;
361
+ }
362
+ /** Format the log output as an ASCII table. */
363
+ table(columns) {
364
+ const { keys, rows } = formatTable(this.#args[0], columns);
365
+ this.#options.tableData = { keys, rows };
366
+ // Override args to show '(table)' label, matching original gg.table() behavior
367
+ this.#args = ['(table)'];
368
+ // Also emit native console.table
369
+ if (columns) {
370
+ console.table(this.#value, columns);
371
+ }
372
+ else {
373
+ console.table(this.#value);
374
+ }
375
+ return this;
376
+ }
377
+ /** Flush the log immediately and return the passthrough value. */
378
+ get v() {
379
+ this.#flush();
380
+ return this.#value;
381
+ }
382
+ #flush() {
383
+ if (this.#flushed)
384
+ return;
385
+ this.#flushed = true;
386
+ ggLog(this.#options, ...this.#args);
387
+ }
388
+ }
269
389
  /**
270
390
  * Core logging function shared by all gg methods.
271
391
  *
272
- * All public methods (gg, gg.ns, gg.warn, gg.error, gg.table, etc.)
273
- * funnel through this function. It handles namespace resolution,
274
- * debug output, capture hook, and passthrough return.
392
+ * Handles namespace resolution, debug output, capture hook, and return value.
275
393
  */
276
394
  function ggLog(options, ...args) {
277
395
  const { ns: nsLabel, file, line, col, src, level, stack, tableData } = options;
@@ -286,24 +404,7 @@ function ggLog(options, ...args) {
286
404
  namespaceToLogFunction.set(namespace, createGgDebugger(namespace)).get(namespace);
287
405
  // Prepare args for logging (console output is value-only; src is carried
288
406
  // on CapturedEntry for the Eruda UI to display on hover)
289
- let logArgs;
290
- let returnValue;
291
- if (!args.length) {
292
- // No arguments: return call-site info for open-in-editor
293
- const fileName = file ? file.replace(srcRootRegex, '') : nsLabel;
294
- const functionName = nsLabel.includes('@') ? nsLabel.split('@').pop() || '' : '';
295
- const url = file ? openInEditorUrl(file, line, col) : '';
296
- logArgs = [` 📝 ${nsLabel}`];
297
- returnValue = { fileName, functionName, url };
298
- }
299
- else if (args.length === 1) {
300
- logArgs = [args[0]];
301
- returnValue = args[0];
302
- }
303
- else {
304
- logArgs = [args[0], ...args.slice(1)];
305
- returnValue = args[0];
306
- }
407
+ const logArgs = args.length === 0 ? ['(no args)'] : [...args];
307
408
  // Add level prefix emoji for info/warn/error
308
409
  if (level === 'info') {
309
410
  logArgs[0] = `ℹ️ ${logArgs[0]}`;
@@ -351,22 +452,39 @@ function ggLog(options, ...args) {
351
452
  else {
352
453
  earlyLogBuffer.push(entry);
353
454
  }
354
- return returnValue;
355
455
  }
356
456
  /**
357
457
  * gg._ns() - Internal: log with namespace and source file metadata.
358
458
  *
359
- * Called by the ggCallSitesPlugin Vite plugin, which rewrites both bare gg()
360
- * calls and manual gg.ns() calls to gg._ns({ns, file, line, col}, ...) at
361
- * build time. This gives each call site a unique namespace plus the source
459
+ * Called by the ggCallSitesPlugin Vite plugin, which rewrites bare gg()
460
+ * calls to gg._ns({ns, file, line, col, src}, ...) at build time.
461
+ * This gives each call site a unique namespace plus the source
362
462
  * location for open-in-editor support.
363
463
  *
364
- * @param options - { ns: string; file?: string; line?: number; col?: number }
365
- * @param args - Same arguments as gg()
366
- * @returns Same as gg() - the first arg, or call-site info if no args
464
+ * Returns a GgChain for chaining modifiers (.ns(), .warn(), etc.)
367
465
  */
368
466
  gg._ns = function (options, ...args) {
369
- return ggLog(options, ...args);
467
+ const disabled = !ggConfig.enabled || isCloudflareWorker();
468
+ return new GgChain(args[0], args, options, disabled);
469
+ };
470
+ /**
471
+ * gg._here() - Internal: call-site info with source metadata from Vite plugin.
472
+ *
473
+ * Called by the ggCallSitesPlugin when it rewrites gg.here() calls.
474
+ */
475
+ gg._here = function (options) {
476
+ if (!ggConfig.enabled || isCloudflareWorker()) {
477
+ return { fileName: '', functionName: '', url: '' };
478
+ }
479
+ const { ns: nsLabel, file, line, col } = options;
480
+ const namespace = `gg:${nsLabel}`;
481
+ const ggLogFunction = namespaceToLogFunction.get(namespace) ||
482
+ namespaceToLogFunction.set(namespace, createGgDebugger(namespace)).get(namespace);
483
+ ggLogFunction(` 📝 ${nsLabel}`);
484
+ const fileName = file ? file.replace(srcRootRegex, '') : nsLabel;
485
+ const functionName = nsLabel.includes('@') ? nsLabel.split('@').pop() || '' : '';
486
+ const url = file ? openInEditorUrl(file, line, col) : '';
487
+ return { fileName, functionName, url };
370
488
  };
371
489
  /**
372
490
  * gg._o() - Internal: build options object for gg._ns() without object literal syntax.
@@ -423,181 +541,66 @@ function getErrorStack(firstArg, skipFrames) {
423
541
  }
424
542
  return captureStack(skipFrames);
425
543
  }
544
+ // Timer storage for gg.time / gg.timeEnd / gg.timeLog
545
+ // Maps timer label → { start: number, ns?: string, options?: LogOptions }
546
+ const timers = new Map();
426
547
  /**
427
- * gg.info() - Log at info level.
428
- *
429
- * Passthrough: returns the first argument.
430
- * In Eruda, entries are styled with a blue/info indicator.
431
- *
432
- * @example
433
- * gg.info('System startup complete');
434
- * const config = gg.info(loadedConfig, 'loaded config');
435
- */
436
- gg.info = function (...args) {
437
- if (!ggConfig.enabled || isCloudflareWorker()) {
438
- return args.length ? args[0] : undefined;
439
- }
440
- const callpoint = resolveCallpoint(3);
441
- return ggLog({ ns: callpoint, level: 'info' }, ...args);
442
- };
443
- /**
444
- * gg._info() - Internal: info with call-site metadata from Vite plugin.
445
- */
446
- gg._info = function (options, ...args) {
447
- return ggLog({ ...options, level: 'info' }, ...args);
448
- };
449
- /**
450
- * gg.warn() - Log at warning level.
451
- *
452
- * Passthrough: returns the first argument.
453
- * In Eruda, entries are styled with a yellow/warning indicator.
454
- *
455
- * @example
456
- * gg.warn('deprecated API used');
457
- * const result = gg.warn(computeValue(), 'might be slow');
458
- */
459
- gg.warn = function (...args) {
460
- if (!ggConfig.enabled || isCloudflareWorker()) {
461
- return args.length ? args[0] : undefined;
462
- }
463
- const callpoint = resolveCallpoint(3);
464
- return ggLog({ ns: callpoint, level: 'warn' }, ...args);
465
- };
466
- /**
467
- * gg._warn() - Internal: warn with call-site metadata from Vite plugin.
468
- */
469
- gg._warn = function (options, ...args) {
470
- return ggLog({ ...options, level: 'warn' }, ...args);
471
- };
472
- /**
473
- * gg.error() - Log at error level.
474
- *
475
- * Passthrough: returns the first argument.
476
- * Captures a stack trace silently — visible in Eruda via a collapsible toggle.
477
- * If the first argument is an Error object, its .stack is used instead.
478
- *
479
- * @example
480
- * gg.error('connection failed');
481
- * gg.error(new Error('timeout'));
482
- * const val = gg.error(response, 'unexpected status');
483
- */
484
- gg.error = function (...args) {
485
- if (!ggConfig.enabled || isCloudflareWorker()) {
486
- return args.length ? args[0] : undefined;
487
- }
488
- const callpoint = resolveCallpoint(3);
489
- const stack = getErrorStack(args[0], 4);
490
- return ggLog({ ns: callpoint, level: 'error', stack }, ...args);
491
- };
492
- /**
493
- * gg._error() - Internal: error with call-site metadata from Vite plugin.
494
- */
495
- gg._error = function (options, ...args) {
496
- const stack = getErrorStack(args[0], 3);
497
- return ggLog({ ...options, level: 'error', stack }, ...args);
498
- };
499
- /**
500
- * gg.assert() - Log only if condition is false.
501
- *
502
- * Like console.assert: if the first argument is falsy, logs the remaining
503
- * arguments at error level. If the condition is truthy, does nothing.
504
- * Passthrough: always returns the condition value.
548
+ * Chainable wrapper returned by gg.time(). Only supports .ns() for setting
549
+ * the namespace for the entire timer group (inherited by timeLog/timeEnd).
505
550
  *
506
551
  * @example
507
- * gg.assert(user != null, 'user should exist');
508
- * gg.assert(list.length > 0, 'list is empty', list);
509
- */
510
- gg.assert = function (condition, ...args) {
511
- if (!condition) {
512
- if (!ggConfig.enabled || isCloudflareWorker())
513
- return condition;
514
- const callpoint = resolveCallpoint(3);
515
- const stack = captureStack(4);
516
- const assertArgs = args.length > 0 ? args : ['Assertion failed'];
517
- ggLog({ ns: callpoint, level: 'error', stack }, ...assertArgs);
518
- }
519
- return condition;
520
- };
521
- /**
522
- * gg._assert() - Internal: assert with call-site metadata from Vite plugin.
523
- */
524
- gg._assert = function (options, condition, ...args) {
525
- if (!condition) {
526
- if (!ggConfig.enabled || isCloudflareWorker())
527
- return condition;
528
- const stack = captureStack(3);
529
- const assertArgs = args.length > 0 ? args : ['Assertion failed'];
530
- ggLog({ ...options, level: 'error', stack }, ...assertArgs);
552
+ * gg.time('fetch').ns('api-pipeline')
553
+ * gg.time('fetch').ns('$FN:timers') // template vars work too
554
+ */
555
+ export class GgTimerChain {
556
+ #label;
557
+ #options;
558
+ constructor(label, options) {
559
+ this.#label = label;
560
+ this.#options = options;
561
+ }
562
+ /** Set a custom namespace for this timer group.
563
+ * Supports the same template variables as GgChain.ns().
564
+ */
565
+ ns(label) {
566
+ const resolved = resolveNsTemplateVars(label, this.#options);
567
+ const timer = timers.get(this.#label);
568
+ if (timer)
569
+ timer.ns = resolved;
570
+ return this;
531
571
  }
532
- return condition;
533
- };
572
+ }
534
573
  /**
535
- * gg.table() - Log tabular data.
574
+ * gg.time() - Start a named timer. Returns a GgTimerChain for optional .ns() chaining.
536
575
  *
537
- * Formats an array of objects (or an object of objects) as an ASCII table.
538
- * Passthrough: returns the data argument.
539
- *
540
- * @example
541
- * gg.table([{name: 'Alice', age: 30}, {name: 'Bob', age: 25}]);
542
- * gg.table({a: {x: 1}, b: {x: 2}});
543
- */
544
- gg.table = function (data, columns) {
545
- if (!ggConfig.enabled || isCloudflareWorker())
546
- return data;
547
- const callpoint = resolveCallpoint(3);
548
- const { keys, rows } = formatTable(data, columns);
549
- ggLog({ ns: callpoint, tableData: { keys, rows } }, '(table)');
550
- // Also emit a native console.table for proper rendering in browser/Node consoles
551
- if (columns) {
552
- console.table(data, columns);
553
- }
554
- else {
555
- console.table(data);
556
- }
557
- return data;
558
- };
559
- /**
560
- * gg._table() - Internal: table with call-site metadata from Vite plugin.
561
- */
562
- gg._table = function (options, data, columns) {
563
- if (!ggConfig.enabled || isCloudflareWorker())
564
- return data;
565
- const { keys, rows } = formatTable(data, columns);
566
- ggLog({ ...options, tableData: { keys, rows } }, '(table)');
567
- if (columns) {
568
- console.table(data, columns);
569
- }
570
- else {
571
- console.table(data);
572
- }
573
- return data;
574
- };
575
- // Timer storage for gg.time / gg.timeEnd / gg.timeLog
576
- const timers = new Map();
577
- /**
578
- * gg.time() - Start a named timer.
576
+ * @param label - Timer label (default: 'default')
579
577
  *
580
578
  * @example
581
- * gg.time('fetch');
582
- * const data = await fetchData();
583
- * gg.timeEnd('fetch'); // logs "+123ms fetch: 456ms"
579
+ * gg.time('fetch') // basic timer
580
+ * gg.time('fetch').ns('api-pipeline') // with namespace (inherited by timeLog/timeEnd)
581
+ * gg.time('fetch').ns('$FN:timers') // with template variable (plugin)
584
582
  */
585
583
  gg.time = function (label = 'default') {
586
- if (!ggConfig.enabled || isCloudflareWorker())
587
- return;
588
- timers.set(label, performance.now());
584
+ const options = { ns: resolveCallpoint(3) };
585
+ if (ggConfig.enabled && !isCloudflareWorker()) {
586
+ timers.set(label, { start: performance.now(), options });
587
+ }
588
+ return new GgTimerChain(label, options);
589
589
  };
590
590
  /** gg._time() - Internal: time with call-site metadata from Vite plugin. */
591
- gg._time = function (_options, label = 'default') {
592
- if (!ggConfig.enabled || isCloudflareWorker())
593
- return;
594
- timers.set(label, performance.now());
591
+ gg._time = function (options, label = 'default') {
592
+ if (ggConfig.enabled && !isCloudflareWorker()) {
593
+ timers.set(label, { start: performance.now(), options });
594
+ }
595
+ return new GgTimerChain(label, options);
595
596
  };
596
597
  /**
597
598
  * gg.timeLog() - Log the current elapsed time without stopping the timer.
598
599
  *
600
+ * Inherits the namespace set by gg.time().ns() for this timer label.
601
+ *
599
602
  * @example
600
- * gg.time('process');
603
+ * gg.time('process').ns('my-namespace');
601
604
  * // ... step 1 ...
602
605
  * gg.timeLog('process', 'step 1 done');
603
606
  * // ... step 2 ...
@@ -606,92 +609,66 @@ gg._time = function (_options, label = 'default') {
606
609
  gg.timeLog = function (label = 'default', ...args) {
607
610
  if (!ggConfig.enabled || isCloudflareWorker())
608
611
  return;
609
- const start = timers.get(label);
610
- if (start === undefined) {
612
+ const timer = timers.get(label);
613
+ if (timer === undefined) {
611
614
  const callpoint = resolveCallpoint(3);
612
615
  ggLog({ ns: callpoint, level: 'warn' }, `Timer '${label}' does not exist`);
613
616
  return;
614
617
  }
615
- const elapsed = performance.now() - start;
616
- const callpoint = resolveCallpoint(3);
617
- ggLog({ ns: callpoint }, `${label}: ${formatElapsed(elapsed)}`, ...args);
618
+ const elapsed = performance.now() - timer.start;
619
+ const ns = timer.ns ?? timer.options?.ns ?? resolveCallpoint(3);
620
+ ggLog({ ...timer.options, ns }, `${label}: ${formatElapsed(elapsed)}`, ...args);
618
621
  };
619
622
  /** gg._timeLog() - Internal: timeLog with call-site metadata from Vite plugin. */
620
623
  gg._timeLog = function (options, label = 'default', ...args) {
621
624
  if (!ggConfig.enabled || isCloudflareWorker())
622
625
  return;
623
- const start = timers.get(label);
624
- if (start === undefined) {
626
+ const timer = timers.get(label);
627
+ if (timer === undefined) {
625
628
  ggLog({ ...options, level: 'warn' }, `Timer '${label}' does not exist`);
626
629
  return;
627
630
  }
628
- const elapsed = performance.now() - start;
629
- ggLog(options, `${label}: ${formatElapsed(elapsed)}`, ...args);
631
+ const elapsed = performance.now() - timer.start;
632
+ const ns = timer.ns ?? timer.options?.ns ?? options.ns;
633
+ ggLog({ ...options, ns }, `${label}: ${formatElapsed(elapsed)}`, ...args);
630
634
  };
631
635
  /**
632
636
  * gg.timeEnd() - Stop a named timer and log the elapsed time.
633
637
  *
638
+ * Inherits the namespace set by gg.time().ns() for this timer label.
639
+ *
634
640
  * @example
635
- * gg.time('fetch');
641
+ * gg.time('fetch').ns('api-pipeline');
636
642
  * const data = await fetchData();
637
- * gg.timeEnd('fetch'); // logs "fetch: 456.12ms"
643
+ * gg.timeEnd('fetch'); // logs under 'api-pipeline' namespace
638
644
  */
639
645
  gg.timeEnd = function (label = 'default') {
640
646
  if (!ggConfig.enabled || isCloudflareWorker())
641
647
  return;
642
- const start = timers.get(label);
643
- if (start === undefined) {
648
+ const timer = timers.get(label);
649
+ if (timer === undefined) {
644
650
  const callpoint = resolveCallpoint(3);
645
651
  ggLog({ ns: callpoint, level: 'warn' }, `Timer '${label}' does not exist`);
646
652
  return;
647
653
  }
648
- const elapsed = performance.now() - start;
654
+ const elapsed = performance.now() - timer.start;
649
655
  timers.delete(label);
650
- const callpoint = resolveCallpoint(3);
651
- ggLog({ ns: callpoint }, `${label}: ${formatElapsed(elapsed)}`);
656
+ const ns = timer.ns ?? timer.options?.ns ?? resolveCallpoint(3);
657
+ ggLog({ ...timer.options, ns }, `${label}: ${formatElapsed(elapsed)}`);
652
658
  };
653
659
  /** gg._timeEnd() - Internal: timeEnd with call-site metadata from Vite plugin. */
654
660
  gg._timeEnd = function (options, label = 'default') {
655
661
  if (!ggConfig.enabled || isCloudflareWorker())
656
662
  return;
657
- const start = timers.get(label);
658
- if (start === undefined) {
663
+ const timer = timers.get(label);
664
+ if (timer === undefined) {
659
665
  ggLog({ ...options, level: 'warn' }, `Timer '${label}' does not exist`);
660
666
  return;
661
667
  }
662
- const elapsed = performance.now() - start;
668
+ const elapsed = performance.now() - timer.start;
663
669
  timers.delete(label);
664
- ggLog(options, `${label}: ${formatElapsed(elapsed)}`);
665
- };
666
- /**
667
- * gg.trace() - Log with a stack trace.
668
- *
669
- * Like console.trace: logs the arguments plus a full stack trace.
670
- * Passthrough: returns the first argument.
671
- *
672
- * @example
673
- * gg.trace('how did we get here?');
674
- * const val = gg.trace(result, 'call path');
675
- */
676
- gg.trace = function (...args) {
677
- if (!ggConfig.enabled || isCloudflareWorker()) {
678
- return args.length ? args[0] : undefined;
679
- }
680
- const callpoint = resolveCallpoint(3);
681
- const stack = captureStack(4);
682
- const traceArgs = args.length > 0 ? args : ['Trace'];
683
- return ggLog({ ns: callpoint, stack }, ...traceArgs);
684
- };
685
- /**
686
- * gg._trace() - Internal: trace with call-site metadata from Vite plugin.
687
- */
688
- gg._trace = function (options, ...args) {
689
- if (!ggConfig.enabled || isCloudflareWorker()) {
690
- return args.length ? args[0] : undefined;
691
- }
692
- const stack = captureStack(3);
693
- const traceArgs = args.length > 0 ? args : ['Trace'];
694
- return ggLog({ ...options, stack }, ...traceArgs);
670
+ const ns = timer.ns ?? timer.options?.ns ?? options.ns;
671
+ ggLog({ ...options, ns }, `${label}: ${formatElapsed(elapsed)}`);
695
672
  };
696
673
  /**
697
674
  * Format elapsed time with appropriate precision.
@@ -986,8 +963,9 @@ export async function runGgDiagnostics() {
986
963
  if (!ggConfig.showHints || isCloudflareWorker() || diagnosticsRan)
987
964
  return;
988
965
  diagnosticsRan = true;
989
- // Ensure server modules (dotenv) are loaded before diagnostics
966
+ // Ensure server modules (dotenv) and debug factory are loaded before diagnostics
990
967
  await serverModulesReady;
968
+ await debugReady;
991
969
  // Create test debugger for server-side enabled check
992
970
  const ggLogTest = debugFactory('gg:TEST');
993
971
  let ggMessage = '\n';
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { gg, fg, bg, bold, italic, underline, dim } from './gg.js';
1
+ import { gg, GgChain, GgTimerChain, fg, bg, bold, italic, underline, dim } from './gg.js';
2
2
  import openInEditorPlugin from './open-in-editor.js';
3
3
  import ggCallSitesPlugin from './gg-call-sites-plugin.js';
4
4
  export { default as GgConsole } from './GgConsole.svelte';
5
- export { gg, fg, bg, bold, italic, underline, dim, openInEditorPlugin, ggCallSitesPlugin };
5
+ export { gg, GgChain, GgTimerChain, fg, bg, bold, italic, underline, dim, openInEditorPlugin, ggCallSitesPlugin };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // Reexport your entry components here
2
- import { gg, fg, bg, bold, italic, underline, dim } from './gg.js';
2
+ import { gg, GgChain, GgTimerChain, fg, bg, bold, italic, underline, dim } from './gg.js';
3
3
  import openInEditorPlugin from './open-in-editor.js';
4
4
  import ggCallSitesPlugin from './gg-call-sites-plugin.js';
5
5
  export { default as GgConsole } from './GgConsole.svelte';
6
- export { gg, fg, bg, bold, italic, underline, dim, openInEditorPlugin, ggCallSitesPlugin };
6
+ export { gg, GgChain, GgTimerChain, fg, bg, bold, italic, underline, dim, openInEditorPlugin, ggCallSitesPlugin };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leftium/gg",
3
- "version": "0.0.46",
3
+ "version": "0.0.48",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/Leftium/gg.git"