@leftium/gg 0.0.27 → 0.0.29

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
@@ -20,7 +20,67 @@ npm add @leftium/gg
20
20
 
21
21
  ## Usage
22
22
 
23
- _Coming soon..._
23
+ ### Basic Logging
24
+
25
+ ```javascript
26
+ import { gg } from '@leftium/gg';
27
+
28
+ // Simple logging
29
+ gg('Hello world');
30
+
31
+ // Log expressions (returns first argument)
32
+ const result = gg(someFunction());
33
+
34
+ // Multiple arguments
35
+ gg('User:', user, 'Status:', status);
36
+ ```
37
+
38
+ ### Color Support (ANSI)
39
+
40
+ Color your logs for better visual distinction using `fg()` (foreground/text) and `bg()` (background):
41
+
42
+ ```javascript
43
+ import { gg, fg, bg } from '@leftium/gg';
44
+
45
+ // Simple foreground/background colors
46
+ gg(fg('red')`Error occurred`);
47
+ gg(bg('yellow')`Warning message`);
48
+
49
+ // Method chaining (order doesn't matter!)
50
+ gg(fg('white').bg('red')`Critical error!`);
51
+ gg(bg('green').fg('white')`Success message`);
52
+
53
+ // Define reusable color schemes
54
+ const input = fg('blue').bg('yellow');
55
+ const transcript = bg('green').fg('white');
56
+ const error = fg('white').bg('red');
57
+
58
+ gg(input`User input message`);
59
+ gg(transcript`AI transcript response`);
60
+ gg(error`Something went wrong`);
61
+
62
+ // Mix colored and normal text
63
+ gg(fg('red')`Error: ` + bg('yellow')`warning` + ' normal text');
64
+
65
+ // Custom hex colors with chaining
66
+ gg(fg('#ff6347').bg('#98fb98')`Custom colors`);
67
+
68
+ // RGB colors
69
+ gg(fg('rgb(255,99,71)')`Tomato text`);
70
+ ```
71
+
72
+ **Supported color formats:**
73
+
74
+ - Named colors: `'red'`, `'green'`, `'blue'`, `'cyan'`, `'magenta'`, `'yellow'`, `'white'`, `'black'`, `'gray'`, `'orange'`, `'purple'`, `'pink'`
75
+ - Hex codes: `'#ff0000'`, `'#f00'`
76
+ - RGB: `'rgb(255,0,0)'`, `'rgba(255,0,0,0.5)'`
77
+
78
+ **Where colors work:**
79
+
80
+ - ✅ Native browser console (Chrome DevTools, Firefox, etc.)
81
+ - ✅ Eruda GG panel (mobile debugging)
82
+ - ✅ Node.js terminal
83
+ - ✅ All environments that support ANSI escape codes
24
84
 
25
85
  ## Technical Details
26
86
 
@@ -570,7 +570,8 @@ export function createGgPlugin(options, gg) {
570
570
  if (typeof arg === 'object' && arg !== null) {
571
571
  return JSON.stringify(arg);
572
572
  }
573
- return String(arg);
573
+ // Strip ANSI escape codes from string args
574
+ return stripAnsi(String(arg));
574
575
  })
575
576
  .join(' ');
576
577
  return `${time} ${ns} ${argsStr}`;
@@ -715,13 +716,13 @@ export function createGgPlugin(options, gg) {
715
716
  return `<span style="color: #888; cursor: pointer; text-decoration: underline;" class="gg-expand" data-index="${uniqueId}">${preview}</span>`;
716
717
  }
717
718
  else {
718
- // Convert URLs to clickable links
719
+ // Parse ANSI codes first, then convert URLs to clickable links
719
720
  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>`;
721
+ const parsedAnsi = parseAnsiToHtml(argStr);
722
+ // Note: URL linking happens after ANSI parsing, so links work inside colored text
723
+ // This is a simple approach - URLs inside ANSI codes won't be linkified
724
+ // For more complex parsing, we'd need to track ANSI state while matching URLs
725
+ return `<span>${parsedAnsi}</span>`;
725
726
  }
726
727
  })
727
728
  .join(' ');
@@ -773,5 +774,72 @@ export function createGgPlugin(options, gg) {
773
774
  div.textContent = text;
774
775
  return div.innerHTML;
775
776
  }
777
+ /**
778
+ * Strip ANSI escape codes from text
779
+ * Removes all ANSI escape sequences like \x1b[...m
780
+ */
781
+ function stripAnsi(text) {
782
+ // Remove all ANSI escape codes
783
+ return text.replace(/\x1b\[[0-9;]*m/g, '');
784
+ }
785
+ /**
786
+ * Parse ANSI escape codes and convert to HTML with inline styles
787
+ * Supports:
788
+ * - 24-bit RGB: \x1b[38;2;r;g;bm (foreground), \x1b[48;2;r;g;bm (background)
789
+ * - Reset: \x1b[0m
790
+ */
791
+ function parseAnsiToHtml(text) {
792
+ // ANSI escape sequence regex
793
+ // Matches: \x1b[38;2;r;g;bm, \x1b[48;2;r;g;bm, \x1b[0m
794
+ const ansiRegex = /\x1b\[([0-9;]+)m/g;
795
+ let html = '';
796
+ let lastIndex = 0;
797
+ let currentFg = null;
798
+ let currentBg = null;
799
+ let match;
800
+ while ((match = ansiRegex.exec(text)) !== null) {
801
+ // Add text before this code (with current styling)
802
+ const textBefore = text.slice(lastIndex, match.index);
803
+ if (textBefore) {
804
+ html += wrapWithStyle(escapeHtml(textBefore), currentFg, currentBg);
805
+ }
806
+ // Parse the ANSI code
807
+ const code = match[1];
808
+ const parts = code.split(';').map(Number);
809
+ if (parts[0] === 0) {
810
+ // Reset
811
+ currentFg = null;
812
+ currentBg = null;
813
+ }
814
+ else if (parts[0] === 38 && parts[1] === 2 && parts.length >= 5) {
815
+ // Foreground RGB: 38;2;r;g;b
816
+ currentFg = `rgb(${parts[2]},${parts[3]},${parts[4]})`;
817
+ }
818
+ else if (parts[0] === 48 && parts[1] === 2 && parts.length >= 5) {
819
+ // Background RGB: 48;2;r;g;b
820
+ currentBg = `rgb(${parts[2]},${parts[3]},${parts[4]})`;
821
+ }
822
+ lastIndex = ansiRegex.lastIndex;
823
+ }
824
+ // Add remaining text
825
+ const remaining = text.slice(lastIndex);
826
+ if (remaining) {
827
+ html += wrapWithStyle(escapeHtml(remaining), currentFg, currentBg);
828
+ }
829
+ return html || escapeHtml(text);
830
+ }
831
+ /**
832
+ * Wrap text with inline color styles
833
+ */
834
+ function wrapWithStyle(text, fg, bg) {
835
+ if (!fg && !bg)
836
+ return text;
837
+ const styles = [];
838
+ if (fg)
839
+ styles.push(`color: ${fg}`);
840
+ if (bg)
841
+ styles.push(`background-color: ${bg}`);
842
+ return `<span style="${styles.join('; ')}">${text}</span>`;
843
+ }
776
844
  return plugin;
777
845
  }
@@ -0,0 +1,31 @@
1
+ import type { Plugin } from 'vite';
2
+ export interface GgCallSitesPluginOptions {
3
+ /**
4
+ * Pattern to strip from file paths to produce short callpoints.
5
+ * Should match up to and including the source root folder.
6
+ *
7
+ * Default: /.*?(\/(?:src|chunks)\/)/ which strips everything up to "src/" or "chunks/",
8
+ * matching the dev-mode behavior of gg().
9
+ *
10
+ * Example: "/Users/me/project/src/routes/+page.svelte" → "routes/+page.svelte"
11
+ */
12
+ srcRootPattern?: string;
13
+ }
14
+ /**
15
+ * Vite plugin that rewrites bare `gg(...)` calls to `gg.ns('callpoint', ...)`
16
+ * at build time. This gives each call site a unique namespace with zero runtime
17
+ * cost — no stack trace parsing needed.
18
+ *
19
+ * Works in both dev and prod. When the plugin is installed, `gg.ns()` is called
20
+ * with the callpoint baked in as a string literal. Without the plugin, gg()
21
+ * falls back to runtime stack parsing in dev and bare `gg:` in prod.
22
+ *
23
+ * @example
24
+ * // vite.config.ts
25
+ * import { ggCallSitesPlugin } from '@leftium/gg';
26
+ *
27
+ * export default defineConfig({
28
+ * plugins: [ggCallSitesPlugin()]
29
+ * });
30
+ */
31
+ export default function ggCallSitesPlugin(options?: GgCallSitesPluginOptions): Plugin;
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Vite plugin that rewrites bare `gg(...)` calls to `gg.ns('callpoint', ...)`
3
+ * at build time. This gives each call site a unique namespace with zero runtime
4
+ * cost — no stack trace parsing needed.
5
+ *
6
+ * Works in both dev and prod. When the plugin is installed, `gg.ns()` is called
7
+ * with the callpoint baked in as a string literal. Without the plugin, gg()
8
+ * falls back to runtime stack parsing in dev and bare `gg:` in prod.
9
+ *
10
+ * @example
11
+ * // vite.config.ts
12
+ * import { ggCallSitesPlugin } from '@leftium/gg';
13
+ *
14
+ * export default defineConfig({
15
+ * plugins: [ggCallSitesPlugin()]
16
+ * });
17
+ */
18
+ export default function ggCallSitesPlugin(options = {}) {
19
+ const srcRootPattern = options.srcRootPattern ?? '.*?(/(?:src|chunks)/)';
20
+ const srcRootRegex = new RegExp(srcRootPattern, 'i');
21
+ return {
22
+ name: 'gg-call-sites',
23
+ config() {
24
+ // Set a compile-time flag so gg() can detect the plugin is installed.
25
+ // Vite replaces all occurrences of __GG_TAG_PLUGIN__ with true at build time,
26
+ // before any code executes — no ordering issues.
27
+ return {
28
+ define: {
29
+ __GG_TAG_PLUGIN__: 'true'
30
+ }
31
+ };
32
+ },
33
+ transform(code, id) {
34
+ // Only process JS/TS/Svelte files
35
+ if (!/\.(js|ts|svelte|jsx|tsx|mjs|mts)(\?.*)?$/.test(id))
36
+ return null;
37
+ // Quick bail: no gg calls in this file
38
+ if (!code.includes('gg('))
39
+ return null;
40
+ // Don't transform gg's own source files
41
+ if (id.includes('/lib/gg.') || id.includes('/lib/debug'))
42
+ return null;
43
+ // Build the short callpoint from the file path
44
+ // e.g. "/Users/me/project/src/routes/+page.svelte" → "routes/+page.svelte"
45
+ const shortPath = id.replace(srcRootRegex, '');
46
+ return transformGgCalls(code, shortPath);
47
+ }
48
+ };
49
+ }
50
+ /**
51
+ * Find the enclosing function name for a given position in source code.
52
+ * Scans backwards from the position looking for function/method declarations.
53
+ */
54
+ function findEnclosingFunction(code, position) {
55
+ // Look backwards from the gg( call for the nearest function declaration
56
+ const before = code.slice(0, position);
57
+ // Try several patterns, take the closest (last) match
58
+ // Named function: function handleClick(
59
+ // Arrow in variable: const handleClick = (...) =>
60
+ // Arrow in variable: let handleClick = (...) =>
61
+ // Method shorthand: handleClick() {
62
+ // Method: handleClick: function(
63
+ // Class method: async handleClick(
64
+ const patterns = [
65
+ // function declarations: function foo(
66
+ /function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g,
67
+ // const/let/var assignment to arrow or function: const foo =
68
+ /(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=/g,
69
+ // object method shorthand: foo() { or async foo() {
70
+ /(?:async\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\([^)]*\)\s*\{/g,
71
+ // object property function: foo: function
72
+ /([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:\s*(?:async\s+)?function/g
73
+ ];
74
+ let closestName = '';
75
+ let closestPos = -1;
76
+ for (const pattern of patterns) {
77
+ let match;
78
+ while ((match = pattern.exec(before)) !== null) {
79
+ const name = match[1];
80
+ // Skip common false positives
81
+ if ([
82
+ 'if',
83
+ 'for',
84
+ 'while',
85
+ 'switch',
86
+ 'catch',
87
+ 'return',
88
+ 'import',
89
+ 'export',
90
+ 'from',
91
+ 'new',
92
+ 'typeof',
93
+ 'instanceof',
94
+ 'void',
95
+ 'delete',
96
+ 'throw',
97
+ 'case',
98
+ 'else',
99
+ 'in',
100
+ 'of',
101
+ 'do',
102
+ 'try',
103
+ 'class',
104
+ 'super',
105
+ 'this',
106
+ 'with',
107
+ 'yield',
108
+ 'await',
109
+ 'debugger',
110
+ 'default'
111
+ ].includes(name)) {
112
+ continue;
113
+ }
114
+ if (match.index > closestPos) {
115
+ closestPos = match.index;
116
+ closestName = name;
117
+ }
118
+ }
119
+ }
120
+ return closestName;
121
+ }
122
+ /**
123
+ * Transform gg() calls in source code to gg.ns('callpoint', ...) calls.
124
+ *
125
+ * Handles:
126
+ * - bare gg(...) → gg.ns('callpoint', ...)
127
+ * - gg.ns(...) → left untouched (user-specified namespace)
128
+ * - gg.enable, gg.disable, gg.clearPersist, gg._onLog → left untouched
129
+ * - gg inside strings and comments → left untouched
130
+ */
131
+ function transformGgCalls(code, shortPath) {
132
+ // Match gg( that is:
133
+ // - not preceded by a dot (would be obj.gg() — not our function)
134
+ // - not preceded by a word char (would be dogg() or something)
135
+ // - not followed by a dot before the paren (gg.ns, gg.enable, etc.)
136
+ //
137
+ // We use a manual scan approach to correctly handle strings and comments.
138
+ const result = [];
139
+ let lastIndex = 0;
140
+ let modified = false;
141
+ // States for string/comment tracking
142
+ let i = 0;
143
+ while (i < code.length) {
144
+ // Skip single-line comments
145
+ if (code[i] === '/' && code[i + 1] === '/') {
146
+ const end = code.indexOf('\n', i);
147
+ i = end === -1 ? code.length : end + 1;
148
+ continue;
149
+ }
150
+ // Skip multi-line comments
151
+ if (code[i] === '/' && code[i + 1] === '*') {
152
+ const end = code.indexOf('*/', i + 2);
153
+ i = end === -1 ? code.length : end + 2;
154
+ continue;
155
+ }
156
+ // Skip template literals (backticks)
157
+ if (code[i] === '`') {
158
+ i++;
159
+ let depth = 0;
160
+ while (i < code.length) {
161
+ if (code[i] === '\\') {
162
+ i += 2;
163
+ continue;
164
+ }
165
+ if (code[i] === '$' && code[i + 1] === '{') {
166
+ depth++;
167
+ i += 2;
168
+ continue;
169
+ }
170
+ if (code[i] === '}' && depth > 0) {
171
+ depth--;
172
+ i++;
173
+ continue;
174
+ }
175
+ if (code[i] === '`' && depth === 0) {
176
+ i++;
177
+ break;
178
+ }
179
+ i++;
180
+ }
181
+ continue;
182
+ }
183
+ // Skip strings (single and double quotes)
184
+ if (code[i] === '"' || code[i] === "'") {
185
+ const quote = code[i];
186
+ i++;
187
+ while (i < code.length) {
188
+ if (code[i] === '\\') {
189
+ i += 2;
190
+ continue;
191
+ }
192
+ if (code[i] === quote) {
193
+ i++;
194
+ break;
195
+ }
196
+ i++;
197
+ }
198
+ continue;
199
+ }
200
+ // Look for 'gg(' pattern
201
+ if (code[i] === 'g' && code[i + 1] === 'g' && code[i + 2] === '(') {
202
+ // Check preceding character: must not be a word char or dot
203
+ const prevChar = i > 0 ? code[i - 1] : '';
204
+ if (prevChar && /[a-zA-Z0-9_$.]/.test(prevChar)) {
205
+ i++;
206
+ continue;
207
+ }
208
+ // Check it's not gg.something (gg.ns, gg.enable, etc.)
209
+ // At this point we know code[i..i+2] is "gg(" — it's a bare call
210
+ // Find the enclosing function
211
+ const fnName = findEnclosingFunction(code, i);
212
+ const callpoint = `${shortPath}${fnName ? `@${fnName}` : ''}`;
213
+ const escaped = callpoint.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
214
+ // Emit everything before this match
215
+ result.push(code.slice(lastIndex, i));
216
+ // Replace gg( with gg.ns('callpoint',
217
+ // Need to handle gg() with no args → gg.ns('callpoint')
218
+ // and gg(x) → gg.ns('callpoint', x)
219
+ // Peek ahead to check if it's gg() with no args
220
+ const afterParen = code.indexOf(')', i + 3);
221
+ const betweenParens = code.slice(i + 3, afterParen);
222
+ const isNoArgs = betweenParens.trim() === '';
223
+ if (isNoArgs && afterParen !== -1 && !betweenParens.includes('(')) {
224
+ // gg() → gg.ns('callpoint')
225
+ result.push(`gg.ns('${escaped}')`);
226
+ lastIndex = afterParen + 1;
227
+ i = afterParen + 1;
228
+ }
229
+ else {
230
+ // gg(args...) → gg.ns('callpoint', args...)
231
+ result.push(`gg.ns('${escaped}', `);
232
+ lastIndex = i + 3; // skip past "gg("
233
+ i = i + 3;
234
+ }
235
+ modified = true;
236
+ continue;
237
+ }
238
+ i++;
239
+ }
240
+ if (!modified)
241
+ return null;
242
+ result.push(code.slice(lastIndex));
243
+ return { code: result.join(''), map: null };
244
+ }
package/dist/gg.d.ts CHANGED
@@ -23,8 +23,68 @@ export declare namespace gg {
23
23
  var enable: (namespaces: string) => void;
24
24
  var clearPersist: () => void;
25
25
  }
26
+ /**
27
+ * ANSI Color Helpers for gg()
28
+ *
29
+ * Create reusable color schemes with foreground (fg) and background (bg) colors.
30
+ * Works in both native console and Eruda plugin.
31
+ *
32
+ * @example
33
+ * // Method chaining (order doesn't matter)
34
+ * gg(fg('white').bg('red')`Critical error!`);
35
+ * gg(bg('green').fg('white')`Success!`);
36
+ *
37
+ * @example
38
+ * // Define color schemes once, reuse everywhere
39
+ * const input = fg('blue').bg('yellow');
40
+ * const transcript = bg('green').fg('white');
41
+ * const error = fg('white').bg('red');
42
+ *
43
+ * gg(input`User said: hello`);
44
+ * gg(transcript`AI responded: hi`);
45
+ * gg(error`Something broke!`);
46
+ *
47
+ * @example
48
+ * // Mix colored and normal text inline
49
+ * gg(fg('red')`Error: ` + bg('yellow')`warning` + ' normal text');
50
+ *
51
+ * @example
52
+ * // Custom colors (hex, rgb, or named)
53
+ * gg(fg('#ff6347').bg('#98fb98')`Custom colors`);
54
+ *
55
+ * @example
56
+ * // Just foreground or background
57
+ * gg(fg('cyan')`Cyan text`);
58
+ * gg(bg('magenta')`Magenta background`);
59
+ */
60
+ type ColorTagFunction = (strings: TemplateStringsArray, ...values: unknown[]) => string;
61
+ interface ChainableColorFn extends ColorTagFunction {
62
+ fg: (color: string) => ChainableColorFn;
63
+ bg: (color: string) => ChainableColorFn;
64
+ }
65
+ /**
66
+ * Foreground (text) color helper
67
+ * Can be used directly or chained with .bg()
68
+ *
69
+ * @param color - Named color, hex (#rrggbb), or rgb(r,g,b)
70
+ * @example
71
+ * gg(fg('red')`Error`);
72
+ * gg(fg('white').bg('red')`Critical!`);
73
+ */
74
+ export declare function fg(color: string): ChainableColorFn;
75
+ /**
76
+ * Background color helper
77
+ * Can be used directly or chained with .fg()
78
+ *
79
+ * @param color - Named color, hex (#rrggbb), or rgb(r,g,b)
80
+ * @example
81
+ * gg(bg('yellow')`Warning`);
82
+ * gg(bg('green').fg('white')`Success!`);
83
+ */
84
+ export declare function bg(color: string): ChainableColorFn;
26
85
  export declare namespace gg {
27
86
  let _onLog: OnLogCallback | null;
87
+ let ns: (nsLabel: string, ...args: unknown[]) => unknown;
28
88
  }
29
89
  /**
30
90
  * Run gg diagnostics and log configuration status
package/dist/gg.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import debugFactory from './debug.js';
2
2
  import ErrorStackParser from 'error-stack-parser';
3
3
  import { BROWSER, DEV } from 'esm-env';
4
+ const _ggCallSitesPlugin = typeof __GG_TAG_PLUGIN__ !== 'undefined' ? __GG_TAG_PLUGIN__ : false;
4
5
  /**
5
6
  * Creates a debug instance with custom formatArgs to add namespace padding
6
7
  * Padding is done at format time, not in the namespace itself, to keep colors stable
@@ -185,9 +186,12 @@ export function gg(...args) {
185
186
  let url = '';
186
187
  let stack = [];
187
188
  let namespace = 'gg:';
188
- // In development: calculate detailed callpoint information
189
- // In production: skip expensive stack parsing and use simple namespace
190
- if (DEV) {
189
+ // When ggCallSitesPlugin is installed, all bare gg() calls are rewritten to gg.ns()
190
+ // at build time, so this code path only runs for un-transformed calls.
191
+ // Skip expensive stack parsing if the plugin is handling callpoints.
192
+ // In development without plugin: calculate detailed callpoint information
193
+ // In production without plugin: skip expensive stack parsing and use simple namespace
194
+ if (DEV && !_ggCallSitesPlugin) {
191
195
  // Ignore first stack frame, which is always the call to gg() itself.
192
196
  stack = ErrorStackParser.parse(new Error()).splice(1);
193
197
  // Example: http://localhost:5173/src/routes/+page.svelte
@@ -255,6 +259,71 @@ export function gg(...args) {
255
259
  }
256
260
  return returnValue;
257
261
  }
262
+ /**
263
+ * gg.ns() - Log with an explicit namespace (callpoint label).
264
+ *
265
+ * In production builds, the ggCallSitesPlugin Vite plugin rewrites bare gg() calls
266
+ * to gg.ns('callpoint', ...) so each call site gets a unique namespace even
267
+ * after minification. Users can also call gg.ns() directly to set a meaningful
268
+ * label that survives across builds.
269
+ *
270
+ * @param nsLabel - The namespace label (appears as gg:<nsLabel> in output)
271
+ * @param args - Same arguments as gg()
272
+ * @returns Same as gg() - the first arg, or call-site info if no args
273
+ *
274
+ * @example
275
+ * gg.ns("auth", "login failed") // logs under namespace "gg:auth"
276
+ * gg.ns("cart", item, quantity) // logs under namespace "gg:cart"
277
+ */
278
+ gg.ns = function (nsLabel, ...args) {
279
+ if (!ggConfig.enabled || isCloudflareWorker()) {
280
+ return args.length ? args[0] : { url: '', stack: [] };
281
+ }
282
+ const namespace = `gg:${nsLabel}`;
283
+ if (nsLabel.length < 80 && nsLabel.length > maxCallpointLength) {
284
+ maxCallpointLength = nsLabel.length;
285
+ }
286
+ const ggLogFunction = namespaceToLogFunction.get(namespace) ||
287
+ namespaceToLogFunction.set(namespace, createGgDebugger(namespace)).get(namespace);
288
+ // Prepare args for logging
289
+ let logArgs;
290
+ let returnValue;
291
+ if (!args.length) {
292
+ logArgs = [` 📝 ${nsLabel}`];
293
+ returnValue = { fileName: '', functionName: '', url: '', stack: [] };
294
+ }
295
+ else if (args.length === 1) {
296
+ logArgs = [args[0]];
297
+ returnValue = args[0];
298
+ }
299
+ else {
300
+ logArgs = [args[0], ...args.slice(1)];
301
+ returnValue = args[0];
302
+ }
303
+ // Log to console via debug
304
+ if (logArgs.length === 1) {
305
+ ggLogFunction(logArgs[0]);
306
+ }
307
+ else {
308
+ ggLogFunction(logArgs[0], ...logArgs.slice(1));
309
+ }
310
+ // Call capture hook if registered (for Eruda plugin)
311
+ const entry = {
312
+ namespace,
313
+ color: ggLogFunction.color,
314
+ diff: ggLogFunction.diff || 0,
315
+ message: logArgs.length === 1 ? String(logArgs[0]) : logArgs.map(String).join(' '),
316
+ args: logArgs,
317
+ timestamp: Date.now()
318
+ };
319
+ if (_onLogCallback) {
320
+ _onLogCallback(entry);
321
+ }
322
+ else {
323
+ earlyLogBuffer.push(entry);
324
+ }
325
+ return returnValue;
326
+ };
258
327
  gg.disable = isCloudflareWorker() ? () => { } : debugFactory.disable;
259
328
  gg.enable = isCloudflareWorker() ? () => { } : debugFactory.enable;
260
329
  /**
@@ -272,6 +341,120 @@ gg.clearPersist = () => {
272
341
  }
273
342
  }
274
343
  };
344
+ /**
345
+ * Parse color string to RGB values
346
+ * Accepts: named colors, hex (#rgb, #rrggbb), rgb(r,g,b), rgba(r,g,b,a)
347
+ */
348
+ function parseColor(color) {
349
+ // Named colors map (basic ANSI colors + common web colors)
350
+ const namedColors = {
351
+ black: '#000000',
352
+ red: '#ff0000',
353
+ green: '#00ff00',
354
+ yellow: '#ffff00',
355
+ blue: '#0000ff',
356
+ magenta: '#ff00ff',
357
+ cyan: '#00ffff',
358
+ white: '#ffffff',
359
+ // Bright variants
360
+ brightBlack: '#808080',
361
+ brightRed: '#ff6666',
362
+ brightGreen: '#66ff66',
363
+ brightYellow: '#ffff66',
364
+ brightBlue: '#6666ff',
365
+ brightMagenta: '#ff66ff',
366
+ brightCyan: '#66ffff',
367
+ brightWhite: '#ffffff',
368
+ // Common aliases
369
+ gray: '#808080',
370
+ grey: '#808080',
371
+ orange: '#ffa500',
372
+ purple: '#800080',
373
+ pink: '#ffc0cb'
374
+ };
375
+ // Check named colors first
376
+ const normalized = color.toLowerCase().trim();
377
+ if (namedColors[normalized]) {
378
+ color = namedColors[normalized];
379
+ }
380
+ // Parse hex color
381
+ const hexMatch = color.match(/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i);
382
+ if (hexMatch) {
383
+ return {
384
+ r: parseInt(hexMatch[1], 16),
385
+ g: parseInt(hexMatch[2], 16),
386
+ b: parseInt(hexMatch[3], 16)
387
+ };
388
+ }
389
+ // Parse short hex (#rgb)
390
+ const shortHexMatch = color.match(/^#?([a-f\d])([a-f\d])([a-f\d])$/i);
391
+ if (shortHexMatch) {
392
+ return {
393
+ r: parseInt(shortHexMatch[1] + shortHexMatch[1], 16),
394
+ g: parseInt(shortHexMatch[2] + shortHexMatch[2], 16),
395
+ b: parseInt(shortHexMatch[3] + shortHexMatch[3], 16)
396
+ };
397
+ }
398
+ // Parse rgb(r,g,b) or rgba(r,g,b,a)
399
+ const rgbMatch = color.match(/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i);
400
+ if (rgbMatch) {
401
+ return {
402
+ r: parseInt(rgbMatch[1]),
403
+ g: parseInt(rgbMatch[2]),
404
+ b: parseInt(rgbMatch[3])
405
+ };
406
+ }
407
+ return null;
408
+ }
409
+ /**
410
+ * Internal helper to create chainable color function with method chaining
411
+ */
412
+ function createColorFunction(fgCode = '', bgCode = '') {
413
+ const tagFn = function (strings, ...values) {
414
+ const text = strings.reduce((acc, str, i) => acc + str + (values[i] !== undefined ? String(values[i]) : ''), '');
415
+ return fgCode + bgCode + text + '\x1b[0m';
416
+ };
417
+ // Add method chaining
418
+ tagFn.fg = (color) => {
419
+ const rgb = parseColor(color);
420
+ const newFgCode = rgb ? `\x1b[38;2;${rgb.r};${rgb.g};${rgb.b}m` : '';
421
+ return createColorFunction(newFgCode, bgCode);
422
+ };
423
+ tagFn.bg = (color) => {
424
+ const rgb = parseColor(color);
425
+ const newBgCode = rgb ? `\x1b[48;2;${rgb.r};${rgb.g};${rgb.b}m` : '';
426
+ return createColorFunction(fgCode, newBgCode);
427
+ };
428
+ return tagFn;
429
+ }
430
+ /**
431
+ * Foreground (text) color helper
432
+ * Can be used directly or chained with .bg()
433
+ *
434
+ * @param color - Named color, hex (#rrggbb), or rgb(r,g,b)
435
+ * @example
436
+ * gg(fg('red')`Error`);
437
+ * gg(fg('white').bg('red')`Critical!`);
438
+ */
439
+ export function fg(color) {
440
+ const rgb = parseColor(color);
441
+ const fgCode = rgb ? `\x1b[38;2;${rgb.r};${rgb.g};${rgb.b}m` : '';
442
+ return createColorFunction(fgCode, '');
443
+ }
444
+ /**
445
+ * Background color helper
446
+ * Can be used directly or chained with .fg()
447
+ *
448
+ * @param color - Named color, hex (#rrggbb), or rgb(r,g,b)
449
+ * @example
450
+ * gg(bg('yellow')`Warning`);
451
+ * gg(bg('green').fg('white')`Success!`);
452
+ */
453
+ export function bg(color) {
454
+ const rgb = parseColor(color);
455
+ const bgCode = rgb ? `\x1b[48;2;${rgb.r};${rgb.g};${rgb.b}m` : '';
456
+ return createColorFunction('', bgCode);
457
+ }
275
458
  /**
276
459
  * Hook for capturing gg() output (used by Eruda plugin)
277
460
  * Set this to a callback function to receive log entries
@@ -335,10 +518,6 @@ export async function runGgDiagnostics() {
335
518
  if (BROWSER) {
336
519
  const hint = makeHint(!ggLogTest.enabled, " (Try `localStorage.debug = 'gg:*'`)");
337
520
  message(`${checkbox(ggLogTest.enabled)} localStorage.debug: ${localStorage?.debug}${hint}`);
338
- if (DEV) {
339
- const { status } = await fetch('/__open-in-editor?file=+');
340
- message(makeHint(status === 222, `✅ (optional) open-in-editor vite plugin detected! (status code: ${status})`, `⚠️ (optional) open-in-editor vite plugin not detected. (status code: ${status}.) Add plugin in vite.config.ts`));
341
- }
342
521
  }
343
522
  else {
344
523
  const hint = makeHint(!ggLogTest.enabled, ' (Try `DEBUG=gg:* npm dev`)');
@@ -347,6 +526,12 @@ export async function runGgDiagnostics() {
347
526
  }
348
527
  message(`${checkbox(ggLogTest.enabled)} DEBUG env variable: ${process?.env?.DEBUG}${hint}`);
349
528
  }
529
+ // Optional plugin diagnostics listed last
530
+ message(makeHint(_ggCallSitesPlugin, `✅ (optional) gg-call-sites vite plugin detected! Call-site namespaces baked in at build time.`, `⚠️ (optional) gg-call-sites vite plugin not detected. Add ggCallSitesPlugin() to vite.config.ts for build-time call-site namespaces (needed for useful namespaces in prod, faster/more reliable in dev)`));
531
+ if (BROWSER && DEV) {
532
+ const { status } = await fetch('/__open-in-editor?file=+');
533
+ message(makeHint(status === 222, `✅ (optional) open-in-editor vite plugin detected! (status code: ${status}) Clickable links open source files in editor.`, `⚠️ (optional) open-in-editor vite plugin not detected. (status code: ${status}) Add openInEditorPlugin() to vite.config.ts for clickable links that open source files in editor`));
534
+ }
350
535
  // Use plain console.log for diagnostics - appears in Eruda's Console tab
351
536
  console.log(ggMessage);
352
537
  // Reset namespace width after configuration check
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
- import { gg } from './gg.js';
1
+ import { gg, fg, bg } from './gg.js';
2
2
  import openInEditorPlugin from './open-in-editor.js';
3
- export { gg, openInEditorPlugin };
3
+ import ggCallSitesPlugin from './gg-call-sites-plugin.js';
4
+ export { gg, fg, bg, openInEditorPlugin, ggCallSitesPlugin };
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // Reexport your entry components here
2
- import { gg } from './gg.js';
2
+ import { gg, fg, bg } from './gg.js';
3
3
  import openInEditorPlugin from './open-in-editor.js';
4
- export { gg, openInEditorPlugin };
4
+ import ggCallSitesPlugin from './gg-call-sites-plugin.js';
5
+ export { gg, fg, bg, openInEditorPlugin, ggCallSitesPlugin };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leftium/gg",
3
- "version": "0.0.27",
3
+ "version": "0.0.29",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/Leftium/gg.git"