@leftium/gg 0.0.27 → 0.0.28
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 +61 -1
- package/dist/eruda/plugin.js +75 -7
- package/dist/gg-tag-plugin.d.ts +31 -0
- package/dist/gg-tag-plugin.js +234 -0
- package/dist/gg.d.ts +60 -0
- package/dist/gg.js +179 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,7 +20,67 @@ npm add @leftium/gg
|
|
|
20
20
|
|
|
21
21
|
## Usage
|
|
22
22
|
|
|
23
|
-
|
|
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
|
|
package/dist/eruda/plugin.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
719
|
+
// Parse ANSI codes first, then convert URLs to clickable links
|
|
719
720
|
const argStr = String(arg);
|
|
720
|
-
const
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
return `<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 GgTagPluginOptions {
|
|
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 { ggTagPlugin } from '@leftium/gg';
|
|
26
|
+
*
|
|
27
|
+
* export default defineConfig({
|
|
28
|
+
* plugins: [ggTagPlugin()]
|
|
29
|
+
* });
|
|
30
|
+
*/
|
|
31
|
+
export default function ggTagPlugin(options?: GgTagPluginOptions): Plugin;
|
|
@@ -0,0 +1,234 @@
|
|
|
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 { ggTagPlugin } from '@leftium/gg';
|
|
13
|
+
*
|
|
14
|
+
* export default defineConfig({
|
|
15
|
+
* plugins: [ggTagPlugin()]
|
|
16
|
+
* });
|
|
17
|
+
*/
|
|
18
|
+
export default function ggTagPlugin(options = {}) {
|
|
19
|
+
const srcRootPattern = options.srcRootPattern ?? '.*?(/(?:src|chunks)/)';
|
|
20
|
+
const srcRootRegex = new RegExp(srcRootPattern, 'i');
|
|
21
|
+
return {
|
|
22
|
+
name: 'gg-tag',
|
|
23
|
+
transform(code, id) {
|
|
24
|
+
// Only process JS/TS/Svelte files
|
|
25
|
+
if (!/\.(js|ts|svelte|jsx|tsx|mjs|mts)(\?.*)?$/.test(id))
|
|
26
|
+
return null;
|
|
27
|
+
// Quick bail: no gg calls in this file
|
|
28
|
+
if (!code.includes('gg('))
|
|
29
|
+
return null;
|
|
30
|
+
// Don't transform gg's own source files
|
|
31
|
+
if (id.includes('/lib/gg.') || id.includes('/lib/debug'))
|
|
32
|
+
return null;
|
|
33
|
+
// Build the short callpoint from the file path
|
|
34
|
+
// e.g. "/Users/me/project/src/routes/+page.svelte" → "routes/+page.svelte"
|
|
35
|
+
const shortPath = id.replace(srcRootRegex, '');
|
|
36
|
+
return transformGgCalls(code, shortPath);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Find the enclosing function name for a given position in source code.
|
|
42
|
+
* Scans backwards from the position looking for function/method declarations.
|
|
43
|
+
*/
|
|
44
|
+
function findEnclosingFunction(code, position) {
|
|
45
|
+
// Look backwards from the gg( call for the nearest function declaration
|
|
46
|
+
const before = code.slice(0, position);
|
|
47
|
+
// Try several patterns, take the closest (last) match
|
|
48
|
+
// Named function: function handleClick(
|
|
49
|
+
// Arrow in variable: const handleClick = (...) =>
|
|
50
|
+
// Arrow in variable: let handleClick = (...) =>
|
|
51
|
+
// Method shorthand: handleClick() {
|
|
52
|
+
// Method: handleClick: function(
|
|
53
|
+
// Class method: async handleClick(
|
|
54
|
+
const patterns = [
|
|
55
|
+
// function declarations: function foo(
|
|
56
|
+
/function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g,
|
|
57
|
+
// const/let/var assignment to arrow or function: const foo =
|
|
58
|
+
/(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=/g,
|
|
59
|
+
// object method shorthand: foo() { or async foo() {
|
|
60
|
+
/(?:async\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\([^)]*\)\s*\{/g,
|
|
61
|
+
// object property function: foo: function
|
|
62
|
+
/([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:\s*(?:async\s+)?function/g
|
|
63
|
+
];
|
|
64
|
+
let closestName = '';
|
|
65
|
+
let closestPos = -1;
|
|
66
|
+
for (const pattern of patterns) {
|
|
67
|
+
let match;
|
|
68
|
+
while ((match = pattern.exec(before)) !== null) {
|
|
69
|
+
const name = match[1];
|
|
70
|
+
// Skip common false positives
|
|
71
|
+
if ([
|
|
72
|
+
'if',
|
|
73
|
+
'for',
|
|
74
|
+
'while',
|
|
75
|
+
'switch',
|
|
76
|
+
'catch',
|
|
77
|
+
'return',
|
|
78
|
+
'import',
|
|
79
|
+
'export',
|
|
80
|
+
'from',
|
|
81
|
+
'new',
|
|
82
|
+
'typeof',
|
|
83
|
+
'instanceof',
|
|
84
|
+
'void',
|
|
85
|
+
'delete',
|
|
86
|
+
'throw',
|
|
87
|
+
'case',
|
|
88
|
+
'else',
|
|
89
|
+
'in',
|
|
90
|
+
'of',
|
|
91
|
+
'do',
|
|
92
|
+
'try',
|
|
93
|
+
'class',
|
|
94
|
+
'super',
|
|
95
|
+
'this',
|
|
96
|
+
'with',
|
|
97
|
+
'yield',
|
|
98
|
+
'await',
|
|
99
|
+
'debugger',
|
|
100
|
+
'default'
|
|
101
|
+
].includes(name)) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (match.index > closestPos) {
|
|
105
|
+
closestPos = match.index;
|
|
106
|
+
closestName = name;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return closestName;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Transform gg() calls in source code to gg.ns('callpoint', ...) calls.
|
|
114
|
+
*
|
|
115
|
+
* Handles:
|
|
116
|
+
* - bare gg(...) → gg.ns('callpoint', ...)
|
|
117
|
+
* - gg.ns(...) → left untouched (user-specified namespace)
|
|
118
|
+
* - gg.enable, gg.disable, gg.clearPersist, gg._onLog → left untouched
|
|
119
|
+
* - gg inside strings and comments → left untouched
|
|
120
|
+
*/
|
|
121
|
+
function transformGgCalls(code, shortPath) {
|
|
122
|
+
// Match gg( that is:
|
|
123
|
+
// - not preceded by a dot (would be obj.gg() — not our function)
|
|
124
|
+
// - not preceded by a word char (would be dogg() or something)
|
|
125
|
+
// - not followed by a dot before the paren (gg.ns, gg.enable, etc.)
|
|
126
|
+
//
|
|
127
|
+
// We use a manual scan approach to correctly handle strings and comments.
|
|
128
|
+
const result = [];
|
|
129
|
+
let lastIndex = 0;
|
|
130
|
+
let modified = false;
|
|
131
|
+
// States for string/comment tracking
|
|
132
|
+
let i = 0;
|
|
133
|
+
while (i < code.length) {
|
|
134
|
+
// Skip single-line comments
|
|
135
|
+
if (code[i] === '/' && code[i + 1] === '/') {
|
|
136
|
+
const end = code.indexOf('\n', i);
|
|
137
|
+
i = end === -1 ? code.length : end + 1;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
// Skip multi-line comments
|
|
141
|
+
if (code[i] === '/' && code[i + 1] === '*') {
|
|
142
|
+
const end = code.indexOf('*/', i + 2);
|
|
143
|
+
i = end === -1 ? code.length : end + 2;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
// Skip template literals (backticks)
|
|
147
|
+
if (code[i] === '`') {
|
|
148
|
+
i++;
|
|
149
|
+
let depth = 0;
|
|
150
|
+
while (i < code.length) {
|
|
151
|
+
if (code[i] === '\\') {
|
|
152
|
+
i += 2;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (code[i] === '$' && code[i + 1] === '{') {
|
|
156
|
+
depth++;
|
|
157
|
+
i += 2;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (code[i] === '}' && depth > 0) {
|
|
161
|
+
depth--;
|
|
162
|
+
i++;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (code[i] === '`' && depth === 0) {
|
|
166
|
+
i++;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
i++;
|
|
170
|
+
}
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
// Skip strings (single and double quotes)
|
|
174
|
+
if (code[i] === '"' || code[i] === "'") {
|
|
175
|
+
const quote = code[i];
|
|
176
|
+
i++;
|
|
177
|
+
while (i < code.length) {
|
|
178
|
+
if (code[i] === '\\') {
|
|
179
|
+
i += 2;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (code[i] === quote) {
|
|
183
|
+
i++;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
i++;
|
|
187
|
+
}
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
// Look for 'gg(' pattern
|
|
191
|
+
if (code[i] === 'g' && code[i + 1] === 'g' && code[i + 2] === '(') {
|
|
192
|
+
// Check preceding character: must not be a word char or dot
|
|
193
|
+
const prevChar = i > 0 ? code[i - 1] : '';
|
|
194
|
+
if (prevChar && /[a-zA-Z0-9_$.]/.test(prevChar)) {
|
|
195
|
+
i++;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
// Check it's not gg.something (gg.ns, gg.enable, etc.)
|
|
199
|
+
// At this point we know code[i..i+2] is "gg(" — it's a bare call
|
|
200
|
+
// Find the enclosing function
|
|
201
|
+
const fnName = findEnclosingFunction(code, i);
|
|
202
|
+
const callpoint = `${shortPath}${fnName ? `@${fnName}` : ''}`;
|
|
203
|
+
const escaped = callpoint.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
204
|
+
// Emit everything before this match
|
|
205
|
+
result.push(code.slice(lastIndex, i));
|
|
206
|
+
// Replace gg( with gg.ns('callpoint',
|
|
207
|
+
// Need to handle gg() with no args → gg.ns('callpoint')
|
|
208
|
+
// and gg(x) → gg.ns('callpoint', x)
|
|
209
|
+
// Peek ahead to check if it's gg() with no args
|
|
210
|
+
const afterParen = code.indexOf(')', i + 3);
|
|
211
|
+
const betweenParens = code.slice(i + 3, afterParen);
|
|
212
|
+
const isNoArgs = betweenParens.trim() === '';
|
|
213
|
+
if (isNoArgs && afterParen !== -1 && !betweenParens.includes('(')) {
|
|
214
|
+
// gg() → gg.ns('callpoint')
|
|
215
|
+
result.push(`gg.ns('${escaped}')`);
|
|
216
|
+
lastIndex = afterParen + 1;
|
|
217
|
+
i = afterParen + 1;
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
// gg(args...) → gg.ns('callpoint', args...)
|
|
221
|
+
result.push(`gg.ns('${escaped}', `);
|
|
222
|
+
lastIndex = i + 3; // skip past "gg("
|
|
223
|
+
i = i + 3;
|
|
224
|
+
}
|
|
225
|
+
modified = true;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
i++;
|
|
229
|
+
}
|
|
230
|
+
if (!modified)
|
|
231
|
+
return null;
|
|
232
|
+
result.push(code.slice(lastIndex));
|
|
233
|
+
return { code: result.join(''), map: null };
|
|
234
|
+
}
|
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
|
@@ -255,6 +255,71 @@ export function gg(...args) {
|
|
|
255
255
|
}
|
|
256
256
|
return returnValue;
|
|
257
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* gg.ns() - Log with an explicit namespace (callpoint label).
|
|
260
|
+
*
|
|
261
|
+
* In production builds, the ggTagPlugin Vite plugin rewrites bare gg() calls
|
|
262
|
+
* to gg.ns('callpoint', ...) so each call site gets a unique namespace even
|
|
263
|
+
* after minification. Users can also call gg.ns() directly to set a meaningful
|
|
264
|
+
* label that survives across builds.
|
|
265
|
+
*
|
|
266
|
+
* @param nsLabel - The namespace label (appears as gg:<nsLabel> in output)
|
|
267
|
+
* @param args - Same arguments as gg()
|
|
268
|
+
* @returns Same as gg() - the first arg, or call-site info if no args
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* gg.ns("auth", "login failed") // logs under namespace "gg:auth"
|
|
272
|
+
* gg.ns("cart", item, quantity) // logs under namespace "gg:cart"
|
|
273
|
+
*/
|
|
274
|
+
gg.ns = function (nsLabel, ...args) {
|
|
275
|
+
if (!ggConfig.enabled || isCloudflareWorker()) {
|
|
276
|
+
return args.length ? args[0] : { url: '', stack: [] };
|
|
277
|
+
}
|
|
278
|
+
const namespace = `gg:${nsLabel}`;
|
|
279
|
+
if (nsLabel.length < 80 && nsLabel.length > maxCallpointLength) {
|
|
280
|
+
maxCallpointLength = nsLabel.length;
|
|
281
|
+
}
|
|
282
|
+
const ggLogFunction = namespaceToLogFunction.get(namespace) ||
|
|
283
|
+
namespaceToLogFunction.set(namespace, createGgDebugger(namespace)).get(namespace);
|
|
284
|
+
// Prepare args for logging
|
|
285
|
+
let logArgs;
|
|
286
|
+
let returnValue;
|
|
287
|
+
if (!args.length) {
|
|
288
|
+
logArgs = [` 📝 ${nsLabel}`];
|
|
289
|
+
returnValue = { fileName: '', functionName: '', url: '', stack: [] };
|
|
290
|
+
}
|
|
291
|
+
else if (args.length === 1) {
|
|
292
|
+
logArgs = [args[0]];
|
|
293
|
+
returnValue = args[0];
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
logArgs = [args[0], ...args.slice(1)];
|
|
297
|
+
returnValue = args[0];
|
|
298
|
+
}
|
|
299
|
+
// Log to console via debug
|
|
300
|
+
if (logArgs.length === 1) {
|
|
301
|
+
ggLogFunction(logArgs[0]);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
ggLogFunction(logArgs[0], ...logArgs.slice(1));
|
|
305
|
+
}
|
|
306
|
+
// Call capture hook if registered (for Eruda plugin)
|
|
307
|
+
const entry = {
|
|
308
|
+
namespace,
|
|
309
|
+
color: ggLogFunction.color,
|
|
310
|
+
diff: ggLogFunction.diff || 0,
|
|
311
|
+
message: logArgs.length === 1 ? String(logArgs[0]) : logArgs.map(String).join(' '),
|
|
312
|
+
args: logArgs,
|
|
313
|
+
timestamp: Date.now()
|
|
314
|
+
};
|
|
315
|
+
if (_onLogCallback) {
|
|
316
|
+
_onLogCallback(entry);
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
earlyLogBuffer.push(entry);
|
|
320
|
+
}
|
|
321
|
+
return returnValue;
|
|
322
|
+
};
|
|
258
323
|
gg.disable = isCloudflareWorker() ? () => { } : debugFactory.disable;
|
|
259
324
|
gg.enable = isCloudflareWorker() ? () => { } : debugFactory.enable;
|
|
260
325
|
/**
|
|
@@ -272,6 +337,120 @@ gg.clearPersist = () => {
|
|
|
272
337
|
}
|
|
273
338
|
}
|
|
274
339
|
};
|
|
340
|
+
/**
|
|
341
|
+
* Parse color string to RGB values
|
|
342
|
+
* Accepts: named colors, hex (#rgb, #rrggbb), rgb(r,g,b), rgba(r,g,b,a)
|
|
343
|
+
*/
|
|
344
|
+
function parseColor(color) {
|
|
345
|
+
// Named colors map (basic ANSI colors + common web colors)
|
|
346
|
+
const namedColors = {
|
|
347
|
+
black: '#000000',
|
|
348
|
+
red: '#ff0000',
|
|
349
|
+
green: '#00ff00',
|
|
350
|
+
yellow: '#ffff00',
|
|
351
|
+
blue: '#0000ff',
|
|
352
|
+
magenta: '#ff00ff',
|
|
353
|
+
cyan: '#00ffff',
|
|
354
|
+
white: '#ffffff',
|
|
355
|
+
// Bright variants
|
|
356
|
+
brightBlack: '#808080',
|
|
357
|
+
brightRed: '#ff6666',
|
|
358
|
+
brightGreen: '#66ff66',
|
|
359
|
+
brightYellow: '#ffff66',
|
|
360
|
+
brightBlue: '#6666ff',
|
|
361
|
+
brightMagenta: '#ff66ff',
|
|
362
|
+
brightCyan: '#66ffff',
|
|
363
|
+
brightWhite: '#ffffff',
|
|
364
|
+
// Common aliases
|
|
365
|
+
gray: '#808080',
|
|
366
|
+
grey: '#808080',
|
|
367
|
+
orange: '#ffa500',
|
|
368
|
+
purple: '#800080',
|
|
369
|
+
pink: '#ffc0cb'
|
|
370
|
+
};
|
|
371
|
+
// Check named colors first
|
|
372
|
+
const normalized = color.toLowerCase().trim();
|
|
373
|
+
if (namedColors[normalized]) {
|
|
374
|
+
color = namedColors[normalized];
|
|
375
|
+
}
|
|
376
|
+
// Parse hex color
|
|
377
|
+
const hexMatch = color.match(/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i);
|
|
378
|
+
if (hexMatch) {
|
|
379
|
+
return {
|
|
380
|
+
r: parseInt(hexMatch[1], 16),
|
|
381
|
+
g: parseInt(hexMatch[2], 16),
|
|
382
|
+
b: parseInt(hexMatch[3], 16)
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
// Parse short hex (#rgb)
|
|
386
|
+
const shortHexMatch = color.match(/^#?([a-f\d])([a-f\d])([a-f\d])$/i);
|
|
387
|
+
if (shortHexMatch) {
|
|
388
|
+
return {
|
|
389
|
+
r: parseInt(shortHexMatch[1] + shortHexMatch[1], 16),
|
|
390
|
+
g: parseInt(shortHexMatch[2] + shortHexMatch[2], 16),
|
|
391
|
+
b: parseInt(shortHexMatch[3] + shortHexMatch[3], 16)
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
// Parse rgb(r,g,b) or rgba(r,g,b,a)
|
|
395
|
+
const rgbMatch = color.match(/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i);
|
|
396
|
+
if (rgbMatch) {
|
|
397
|
+
return {
|
|
398
|
+
r: parseInt(rgbMatch[1]),
|
|
399
|
+
g: parseInt(rgbMatch[2]),
|
|
400
|
+
b: parseInt(rgbMatch[3])
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Internal helper to create chainable color function with method chaining
|
|
407
|
+
*/
|
|
408
|
+
function createColorFunction(fgCode = '', bgCode = '') {
|
|
409
|
+
const tagFn = function (strings, ...values) {
|
|
410
|
+
const text = strings.reduce((acc, str, i) => acc + str + (values[i] !== undefined ? String(values[i]) : ''), '');
|
|
411
|
+
return fgCode + bgCode + text + '\x1b[0m';
|
|
412
|
+
};
|
|
413
|
+
// Add method chaining
|
|
414
|
+
tagFn.fg = (color) => {
|
|
415
|
+
const rgb = parseColor(color);
|
|
416
|
+
const newFgCode = rgb ? `\x1b[38;2;${rgb.r};${rgb.g};${rgb.b}m` : '';
|
|
417
|
+
return createColorFunction(newFgCode, bgCode);
|
|
418
|
+
};
|
|
419
|
+
tagFn.bg = (color) => {
|
|
420
|
+
const rgb = parseColor(color);
|
|
421
|
+
const newBgCode = rgb ? `\x1b[48;2;${rgb.r};${rgb.g};${rgb.b}m` : '';
|
|
422
|
+
return createColorFunction(fgCode, newBgCode);
|
|
423
|
+
};
|
|
424
|
+
return tagFn;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Foreground (text) color helper
|
|
428
|
+
* Can be used directly or chained with .bg()
|
|
429
|
+
*
|
|
430
|
+
* @param color - Named color, hex (#rrggbb), or rgb(r,g,b)
|
|
431
|
+
* @example
|
|
432
|
+
* gg(fg('red')`Error`);
|
|
433
|
+
* gg(fg('white').bg('red')`Critical!`);
|
|
434
|
+
*/
|
|
435
|
+
export function fg(color) {
|
|
436
|
+
const rgb = parseColor(color);
|
|
437
|
+
const fgCode = rgb ? `\x1b[38;2;${rgb.r};${rgb.g};${rgb.b}m` : '';
|
|
438
|
+
return createColorFunction(fgCode, '');
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Background color helper
|
|
442
|
+
* Can be used directly or chained with .fg()
|
|
443
|
+
*
|
|
444
|
+
* @param color - Named color, hex (#rrggbb), or rgb(r,g,b)
|
|
445
|
+
* @example
|
|
446
|
+
* gg(bg('yellow')`Warning`);
|
|
447
|
+
* gg(bg('green').fg('white')`Success!`);
|
|
448
|
+
*/
|
|
449
|
+
export function bg(color) {
|
|
450
|
+
const rgb = parseColor(color);
|
|
451
|
+
const bgCode = rgb ? `\x1b[48;2;${rgb.r};${rgb.g};${rgb.b}m` : '';
|
|
452
|
+
return createColorFunction('', bgCode);
|
|
453
|
+
}
|
|
275
454
|
/**
|
|
276
455
|
* Hook for capturing gg() output (used by Eruda plugin)
|
|
277
456
|
* Set this to a callback function to receive log entries
|
package/dist/index.d.ts
CHANGED
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
|
-
|
|
4
|
+
import ggTagPlugin from './gg-tag-plugin.js';
|
|
5
|
+
export { gg, fg, bg, openInEditorPlugin, ggTagPlugin };
|