@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 +61 -1
- package/dist/eruda/plugin.js +75 -7
- package/dist/gg-call-sites-plugin.d.ts +31 -0
- package/dist/gg-call-sites-plugin.js +244 -0
- package/dist/gg.d.ts +60 -0
- package/dist/gg.js +192 -7
- 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 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
|
-
//
|
|
189
|
-
//
|
|
190
|
-
if
|
|
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
|
-
|
|
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
|
-
|
|
4
|
+
import ggCallSitesPlugin from './gg-call-sites-plugin.js';
|
|
5
|
+
export { gg, fg, bg, openInEditorPlugin, ggCallSitesPlugin };
|